///|
priv struct CanonicalNetworkHttpUrl {
  scheme : String
  host : String
  port : Int
  path : String
  query : String
}

///|
fn BidiProtocol::handle_network_allocate_request_id(
  self : BidiProtocol,
  request_id : Int,
  _params : Json?,
) -> Unit {
  self.send_success(
    request_id,
    Some(
      make_object({
        "request": Json::string(self.next_synthetic_network_request_id()),
      }),
    ),
  )
}

///|
fn BidiProtocol::handle_network_resolve_matching_intercepts(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Unit {
  let map = match params {
    Some(Object(map)) => map
    _ => {
      self.send_error(
        request_id, "invalid argument", "params must be an object",
      )
      return
    }
  }
  let context_id = match map.get("context") {
    Some(String(context_id)) => context_id
    _ => {
      self.send_error(
        request_id, "invalid argument", "context must be a string",
      )
      return
    }
  }
  let phase = match map.get("phase") {
    Some(String(phase)) =>
      if is_valid_network_intercept_phase(phase) {
        phase
      } else {
        self.send_error(request_id, "invalid argument", "phase is invalid")
        return
      }
    _ => {
      self.send_error(request_id, "invalid argument", "phase must be a string")
      return
    }
  }
  let url = match map.get("url") {
    Some(String(url)) => url
    _ => {
      self.send_error(request_id, "invalid argument", "url must be a string")
      return
    }
  }
  let intercepts : Array[Json] = []
  for
    intercept_id in self.resolve_matching_network_intercepts_internal(
      context_id, phase, url,
    ) {
    intercepts.push(Json::string(intercept_id))
  }
  self.send_success(
    request_id,
    Some(make_object({ "intercepts": Json::array(intercepts) })),
  )
}

///|
fn BidiProtocol::handle_network_remember_collected_data(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Unit {
  let map = match params {
    Some(Object(map)) => map
    _ => {
      self.send_error(
        request_id, "invalid argument", "params must be an object",
      )
      return
    }
  }
  let context_id = match map.get("context") {
    Some(String(context_id)) => context_id
    _ => {
      self.send_error(
        request_id, "invalid argument", "context must be a string",
      )
      return
    }
  }
  let request_token = match map.get("request") {
    Some(String(request_token)) => request_token
    _ => {
      self.send_error(
        request_id, "invalid argument", "request must be a string",
      )
      return
    }
  }
  let request_value = match
    get_map_field_with_alias(map, "requestData", "request_data") {
    Some(raw_value) =>
      match
        self.normalize_synthetic_network_bytes_value(
          request_id, raw_value, "requestData",
        ) {
        Some(value) => Some(value)
        None => return
      }
    None => None
  }
  let response_value = match
    get_map_field_with_alias(map, "responseData", "response_data") {
    Some(raw_value) =>
      match
        self.normalize_synthetic_network_bytes_value(
          request_id, raw_value, "responseData",
        ) {
        Some(value) => Some(value)
        None => return
      }
    None => None
  }

  self.remember_network_collected_data_internal(
    context_id, request_token, request_value, response_value,
  )
  self.send_success(request_id, Some(make_object({})))
}

///|
fn BidiProtocol::handle_network_get_data(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Unit {
  let map = match params {
    Some(Object(map)) => map
    _ => {
      self.send_error(
        request_id, "invalid argument", "params must be an object",
      )
      return
    }
  }
  let request_token = match map.get("request") {
    Some(String(request_token)) => request_token
    _ => {
      self.send_error(
        request_id, "invalid argument", "request must be a string",
      )
      return
    }
  }
  let data_type = match get_map_field_with_alias(map, "dataType", "data_type") {
    Some(String(data_type)) =>
      if data_type == "request" || data_type == "response" {
        data_type
      } else {
        self.send_error(request_id, "invalid argument", "dataType is invalid")
        return
      }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "dataType must be a string",
      )
      return
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing dataType")
      return
    }
  }
  let collector_id : String? = match map.get("collector") {
    Some(Null) | None => None
    Some(String(collector_id)) => Some(collector_id)
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "collector must be a string",
      )
      return
    }
  }
  let disown = match map.get("disown") {
    Some(True) => true
    Some(False) | None => false
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "disown must be a boolean",
      )
      return
    }
  }
  if disown && collector_id == None {
    self.send_error(
      request_id, "invalid argument", "disown=true requires collector",
    )
    return
  }
  let request_bucket = match self.network_collected_data.get(request_token) {
    Some(request_bucket) => request_bucket
    None => {
      self.send_error(
        request_id,
        "no such network data",
        "Unknown request id: " + request_token,
      )
      return
    }
  }
  let (selected_collector, result) = match collector_id {
    Some(collector_id) => {
      if !self.network_data_collectors.contains(collector_id) {
        self.send_error(
          request_id,
          "no such network collector",
          "Unknown collector: " + collector_id,
        )
        return
      }
      match request_bucket.get(collector_id) {
        Some(collector_bucket) =>
          match collector_bucket.get(data_type) {
            Some(result) => (collector_id, result)
            None => {
              self.send_error(
                request_id,
                "no such network data",
                "No collected " + data_type + " for request: " + request_token,
              )
              return
            }
          }
        None => {
          self.send_error(
            request_id,
            "no such network data",
            "No collected " + data_type + " for request: " + request_token,
          )
          return
        }
      }
    }
    None => {
      let mut found : (String, Json)? = None
      for candidate_collector in self.list_active_network_collector_ids() {
        match request_bucket.get(candidate_collector) {
          Some(collector_bucket) =>
            match collector_bucket.get(data_type) {
              Some(result) => {
                found = Some((candidate_collector, result))
                break
              }
              None => ()
            }
          None => ()
        }
      }
      match found {
        Some(found) => found
        None => {
          self.send_error(
            request_id,
            "no such network data",
            "No collected " + data_type + " for request: " + request_token,
          )
          return
        }
      }
    }
  }
  if disown {
    match self.network_collected_data.get(request_token) {
      Some(bucket) =>
        match bucket.get(selected_collector) {
          Some(collector_bucket) => {
            let next_collector_bucket = collector_bucket
            next_collector_bucket.remove(data_type)
            let next_bucket = bucket
            if next_collector_bucket.length() == 0 {
              next_bucket.remove(selected_collector)
            } else {
              next_bucket[selected_collector] = next_collector_bucket
            }
            if next_bucket.length() == 0 {
              self.network_collected_data.remove(request_token)
            } else {
              self.network_collected_data[request_token] = next_bucket
            }
          }
          None => ()
        }
      None => ()
    }
  }
  self.send_success(request_id, Some(result))
}

///|
fn BidiProtocol::handle_network_disown_data(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Unit {
  let map = match params {
    Some(Object(map)) => map
    _ => {
      self.send_error(
        request_id, "invalid argument", "params must be an object",
      )
      return
    }
  }
  let request_token = match map.get("request") {
    Some(String(request_token)) => request_token
    _ => {
      self.send_error(
        request_id, "invalid argument", "request must be a string",
      )
      return
    }
  }
  let data_type = match get_map_field_with_alias(map, "dataType", "data_type") {
    Some(String(data_type)) =>
      if data_type == "request" || data_type == "response" {
        data_type
      } else {
        self.send_error(request_id, "invalid argument", "dataType is invalid")
        return
      }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "dataType must be a string",
      )
      return
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing dataType")
      return
    }
  }
  let collector_id = match map.get("collector") {
    Some(String(collector_id)) => collector_id
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "collector must be a string",
      )
      return
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing collector")
      return
    }
  }
  if !self.network_data_collectors.contains(collector_id) {
    self.send_error(
      request_id,
      "no such network collector",
      "Unknown collector: " + collector_id,
    )
    return
  }
  let request_bucket = match self.network_collected_data.get(request_token) {
    Some(request_bucket) => request_bucket
    None => {
      self.send_error(
        request_id,
        "no such network data",
        "Unknown request id: " + request_token,
      )
      return
    }
  }
  let collector_bucket = match request_bucket.get(collector_id) {
    Some(collector_bucket) => collector_bucket
    None => {
      self.send_error(
        request_id,
        "no such network data",
        "No collected " + data_type + " for request: " + request_token,
      )
      return
    }
  }
  if !collector_bucket.contains(data_type) {
    self.send_error(
      request_id,
      "no such network data",
      "No collected " + data_type + " for request: " + request_token,
    )
    return
  }
  let next_collector_bucket = collector_bucket
  next_collector_bucket.remove(data_type)
  let next_bucket = request_bucket
  if next_collector_bucket.length() == 0 {
    next_bucket.remove(collector_id)
  } else {
    next_bucket[collector_id] = next_collector_bucket
  }
  if next_bucket.length() == 0 {
    self.network_collected_data.remove(request_token)
  } else {
    self.network_collected_data[request_token] = next_bucket
  }
  self.send_success(request_id, Some(make_object({})))
}

///|
fn BidiProtocol::list_active_network_intercept_ids(
  self : BidiProtocol,
) -> Array[String] {
  let ids : Array[String] = []
  for i = 1; i < self.next_network_intercept_id; i = i + 1 {
    let intercept_id = left_pad_with_zero(i.to_string(), 32)
    if self.network_intercepts.contains(intercept_id) {
      ids.push(intercept_id)
    }
  }
  ids
}

///|
fn BidiProtocol::next_synthetic_network_request_id(
  self : BidiProtocol,
) -> String {
  let request_id = "request-" + self.next_network_request_id.to_string()
  self.next_network_request_id += 1
  request_id
}

///|
fn BidiProtocol::remember_network_collected_data_internal(
  self : BidiProtocol,
  context_id : String,
  request_token : String,
  request_value : Json?,
  response_value : Json?,
) -> Unit {
  let request_bucket = self.network_collected_data
    .get(request_token)
    .unwrap_or({})
  let ancestry = self.build_context_scope_ancestry_for_matching(context_id)
  let user_context = self.context_user_context
    .get(context_id)
    .unwrap_or("default")
  let next_bucket = request_bucket
  for collector_id in self.list_active_network_collector_ids() {
    match self.network_data_collectors.get(collector_id) {
      Some(Object(options)) =>
        if network_collector_matches_scope(options, ancestry, user_context) {
          let collector_bucket = next_bucket.get(collector_id).unwrap_or({})
          let data_types = network_collector_data_types(options)
          let max_size = network_collector_max_encoded_size(options)
          let next_collector_bucket = collector_bucket
          match request_value {
            Some(request_data) =>
              if array_contains_string(data_types, "request") &&
                network_bytes_value_encoded_size(request_data) <= max_size {
                next_collector_bucket["request"] = request_data
              }
            None => ()
          }
          match response_value {
            Some(response_data) =>
              if array_contains_string(data_types, "response") &&
                network_bytes_value_encoded_size(response_data) <= max_size {
                next_collector_bucket["response"] = response_data
              }
            None => ()
          }
          if next_collector_bucket.length() > 0 {
            next_bucket[collector_id] = next_collector_bucket
          }
        }
      _ => ()
    }
  }
  self.network_collected_data[request_token] = next_bucket
}

///|
fn BidiProtocol::list_active_network_collector_ids(
  self : BidiProtocol,
) -> Array[String] {
  let ids : Array[String] = []
  for i = 1; i < self.next_network_data_collector_id; i = i + 1 {
    let collector_id = "collector-" + i.to_string()
    if self.network_data_collectors.contains(collector_id) {
      ids.push(collector_id)
    }
  }
  ids
}

///|
fn BidiProtocol::build_context_scope_ancestry_for_matching(
  self : BidiProtocol,
  ctx_id : String,
) -> Array[String] {
  let ancestry = if self.manager.has_session(ctx_id) {
    self.build_context_ancestry(ctx_id)
  } else {
    []
  }
  if ctx_id != "" && !array_contains_string(ancestry, ctx_id) {
    ancestry.push(ctx_id)
  }
  ancestry
}

///|
fn BidiProtocol::resolve_matching_network_intercepts_internal(
  self : BidiProtocol,
  context_id : String,
  phase : String,
  url : String,
) -> Array[String] {
  let intercepts : Array[String] = []
  let ancestry = self.build_context_scope_ancestry_for_matching(context_id)
  for intercept_id in self.list_active_network_intercept_ids() {
    match self.network_intercepts.get(intercept_id) {
      Some(Object(options)) =>
        if network_intercept_matches_phase(options, phase) &&
          network_option_contexts_match(options, ancestry) &&
          network_url_patterns_match(options, url) {
          intercepts.push(intercept_id)
        }
      _ => ()
    }
  }
  intercepts
}

///|
fn network_intercept_matches_phase(
  options : Map[String, Json],
  phase : String,
) -> Bool {
  match options.get("phases") {
    Some(Array(phases)) =>
      for item in phases {
        match item {
          String(name) if name == phase => return true
          _ => ()
        }
      }
    _ => ()
  }
  false
}

///|
fn network_option_contexts_match(
  options : Map[String, Json],
  ancestry : Array[String],
) -> Bool {
  match options.get("contexts") {
    Some(Array(contexts)) => {
      if contexts.length() == 0 {
        return true
      }
      for item in contexts {
        match item {
          String(ctx_id) if array_contains_string(ancestry, ctx_id) =>
            return true
          _ => ()
        }
      }
      false
    }
    _ => true
  }
}

///|
fn network_url_patterns_match(
  options : Map[String, Json],
  url : String,
) -> Bool {
  match options.get("urlPatterns") {
    Some(Array(patterns)) => {
      if patterns.length() == 0 {
        return true
      }
      for entry in patterns {
        if network_url_pattern_entry_matches(entry, url) {
          return true
        }
      }
      false
    }
    _ => true
  }
}

///|
fn network_url_pattern_entry_matches(entry : Json, url : String) -> Bool {
  match entry {
    Object(map) =>
      match map.get("type") {
        Some(String("string")) =>
          match map.get("pattern") {
            Some(String(pattern)) =>
              network_string_url_pattern_matches(pattern, url)
            _ => false
          }
        Some(String("pattern")) => network_object_url_pattern_matches(map, url)
        _ => false
      }
    _ => false
  }
}

///|
fn network_string_url_pattern_matches(
  pattern_url : String,
  target_url : String,
) -> Bool {
  match
    (
      canonicalize_network_http_url(pattern_url),
      canonicalize_network_http_url(target_url),
    ) {
    (Some(pattern), Some(target)) =>
      pattern.scheme == target.scheme &&
      pattern.host == target.host &&
      pattern.port == target.port &&
      pattern.path == target.path &&
      pattern.query == target.query
    _ => pattern_url == target_url
  }
}

///|
fn network_object_url_pattern_matches(
  pattern : Map[String, Json],
  target_url : String,
) -> Bool {
  let target = match canonicalize_network_http_url(target_url) {
    Some(target) => target
    None => return false
  }
  let target_path_token = if target.path.has_prefix("/") {
    target.path.unsafe_substring(start=1, end=target.path.length())
  } else {
    target.path
  }
  match pattern.get("protocol") {
    Some(String(protocol)) =>
      if normalize_network_pattern_protocol(protocol) != target.scheme {
        return false
      }
    _ => ()
  }
  match pattern.get("hostname") {
    Some(String(hostname)) =>
      if hostname.to_lower() != target.host {
        return false
      }
    _ => ()
  }
  match pattern.get("port") {
    Some(String(port)) => if port != target.port.to_string() { return false }
    _ => ()
  }
  match pattern.get("pathname") {
    Some(String(pathname)) =>
      if pathname == "" {
        if target.path != "" && target.path != "/" {
          return false
        }
      } else if pathname.has_prefix("/") {
        if target.path != pathname {
          return false
        }
      } else if target_path_token != pathname {
        return false
      }
    _ => ()
  }
  match pattern.get("search") {
    Some(String(search)) =>
      if search == "" {
        if target.query != "" {
          return false
        }
      } else if target.query != search {
        return false
      }
    _ => ()
  }
  true
}

///|
fn canonicalize_network_http_url(url : String) -> CanonicalNetworkHttpUrl? {
  let scheme = match extract_network_url_scheme(url) {
    Some(scheme) =>
      if scheme == "http" || scheme == "https" {
        scheme
      } else {
        return None
      }
    None => return None
  }
  let authority = match extract_http_url_authority(url) {
    Some(authority) => authority
    None => return None
  }
  let host_port = extract_authority_host_port(authority)
  let (host, port) = match parse_network_host_and_port(host_port, scheme) {
    Some(result) => result
    None => return None
  }
  let path = extract_http_url_path(url).unwrap_or("/")
  let query = extract_network_url_query(url)
  Some({ scheme, host, port, path, query })
}

///|
fn extract_network_url_scheme(url : String) -> String? {
  match find_substring(url, "://", 0) {
    Some(idx) => Some(url.unsafe_substring(start=0, end=idx).to_lower())
    None => None
  }
}

///|
fn parse_network_host_and_port(
  host_port : String,
  scheme : String,
) -> (String, Int)? {
  if host_port == "" {
    return None
  }
  let chars = host_port.to_array()
  let mut colon_idx = -1
  let mut colon_count = 0
  for i = 0; i < chars.length(); i = i + 1 {
    if chars[i] == ':' {
      colon_idx = i
      colon_count += 1
    }
  }
  if colon_count > 1 {
    return None
  }
  if colon_idx < 0 {
    return Some((host_port.to_lower(), default_port_for_network_scheme(scheme)))
  }
  let host = host_port.unsafe_substring(start=0, end=colon_idx)
  let port_text = host_port.unsafe_substring(
    start=colon_idx + 1,
    end=host_port.length(),
  )
  let port = match parse_decimal_string(port_text) {
    Some(port) => port
    None => return None
  }
  if host == "" || port < 0 || port > 65535 {
    return None
  }
  Some((host.to_lower(), port))
}

///|
fn default_port_for_network_scheme(scheme : String) -> Int {
  if scheme == "https" {
    443
  } else {
    80
  }
}

///|
fn extract_network_url_query(url : String) -> String {
  let without_fragment = strip_url_fragment(url)
  match find_substring(without_fragment, "?", 0) {
    Some(idx) =>
      without_fragment.unsafe_substring(
        start=idx + 1,
        end=without_fragment.length(),
      )
    None => ""
  }
}

///|
fn normalize_network_pattern_protocol(protocol : String) -> String {
  if protocol.has_suffix(":") {
    protocol.unsafe_substring(start=0, end=protocol.length() - 1).to_lower()
  } else {
    protocol.to_lower()
  }
}

///|
fn network_collector_matches_scope(
  options : Map[String, Json],
  ancestry : Array[String],
  user_context : String,
) -> Bool {
  match options.get("contexts") {
    Some(Array(contexts)) =>
      if contexts.length() > 0 {
        let mut matched = false
        for item in contexts {
          match item {
            String(ctx_id) if array_contains_string(ancestry, ctx_id) => {
              matched = true
              break
            }
            _ => ()
          }
        }
        if !matched {
          return false
        }
      }
    _ => ()
  }
  match options.get("userContexts") {
    Some(Array(user_contexts)) =>
      if user_contexts.length() > 0 {
        let mut matched = false
        for item in user_contexts {
          match item {
            String(candidate) if candidate == user_context => {
              matched = true
              break
            }
            _ => ()
          }
        }
        if !matched {
          return false
        }
      }
    _ => ()
  }
  true
}

///|
fn network_collector_data_types(options : Map[String, Json]) -> Array[String] {
  let data_types : Array[String] = []
  match options.get("dataTypes") {
    Some(Array(items)) =>
      for item in items {
        match item {
          String(name) => data_types.push(name)
          _ => ()
        }
      }
    _ => ()
  }
  data_types
}

///|
fn network_collector_max_encoded_size(options : Map[String, Json]) -> Int {
  match options.get("maxEncodedDataSize") {
    Some(Number(n, ..)) => n.to_int()
    _ => 1000
  }
}

///|
fn network_bytes_value_encoded_size(value : Json) -> Int {
  match value {
    Object(map) =>
      match map.get("value") {
        Some(String(payload)) => js_utf8_byte_length(payload)
        _ => 0
      }
    _ => 0
  }
}

///|
fn array_contains_string(items : Array[String], target : String) -> Bool {
  for item in items {
    if item == target {
      return true
    }
  }
  false
}