///|
pub(all) struct SyntheticNetworkCookie {
  name : String
  value_text : String
  domain : String
  domain_explicit : Bool
  path : String
  http_only : Bool
  secure : Bool
  same_site : String
  expiry : Int?
}

///|
pub(all) struct SyntheticNetworkSetCookieOverrideEntry {
  set_cookie_header_value : String
  cookie : SyntheticNetworkCookie
}

///|
extern "js" fn js_parse_cookie_expiry_seconds(value : String) -> Int =
  #| (value) => {
  #|   const text = String(value ?? "").trim();
  #|   if (text.length === 0) return -1;
  #|   const hasZone = /(?:GMT|UTC|[+-]\d{2}:?\d{2})$/i.test(text);
  #|   const parsed = Date.parse(hasZone ? text : `${text} GMT`);
  #|   if (!Number.isFinite(parsed)) return -1;
  #|   return Math.floor(parsed / 1000);
  #| }

///|
pub fn synthetic_network_extract_set_cookie_entries(
  headers : Array[Json],
  base_url : String,
  now_seconds : Int,
) -> Array[SyntheticNetworkCookie] {
  let cookies : Array[SyntheticNetworkCookie] = []
  for header in headers {
    let map = match header {
      Object(map) => map
      _ => continue
    }
    let name = match map.get("name") {
      Some(String(name)) => name
      _ => continue
    }
    if !is_ascii_case_insensitive_equal(name, "set-cookie") {
      continue
    }
    let value_map = match map.get("value") {
      Some(Object(value_map)) => value_map
      _ => continue
    }
    let value_type = match value_map.get("type") {
      Some(String(value_type)) => value_type
      _ => continue
    }
    if value_type != "string" {
      continue
    }
    let header_value = match value_map.get("value") {
      Some(String(header_value)) => header_value
      _ => continue
    }
    match
      synthetic_network_parse_set_cookie_header_value(
        header_value, base_url, now_seconds,
      ) {
      Some(cookie) => cookies.push(cookie)
      None => ()
    }
  }
  cookies
}

///|
pub fn synthetic_network_parse_set_cookie_header_value(
  header_value : String,
  base_url : String,
  now_seconds : Int,
) -> SyntheticNetworkCookie? {
  synthetic_network_parse_cookie_text(
    header_value, base_url, "/", "none", now_seconds, true,
  )
}

///|
pub fn synthetic_network_parse_document_cookie_assignment(
  cookie_assignment : String,
  base_url : String,
  default_path : String,
  now_seconds : Int,
) -> SyntheticNetworkCookie? {
  synthetic_network_parse_cookie_text(
    cookie_assignment, base_url, default_path, "default", now_seconds, false,
  )
}

///|
pub fn synthetic_network_format_set_cookie_header(
  entry : Map[String, Json],
) -> String {
  let parts : Array[String] = []
  let name = match entry.get("name") {
    Some(String(name)) => name
    _ => ""
  }
  let value = match entry.get("value") {
    Some(String(value)) => value
    _ => ""
  }
  parts.push(name + "=" + value)
  match entry.get("path") {
    Some(String(path)) => parts.push("Path=" + path)
    _ => ()
  }
  match entry.get("domain") {
    Some(String(domain)) => parts.push("Domain=" + domain)
    _ => ()
  }
  match entry.get("expiry") {
    Some(String(expiry)) => parts.push("Expires=" + expiry)
    _ => ()
  }
  match entry.get("maxAge") {
    Some(Number(max_age, ..)) =>
      parts.push("Max-Age=" + max_age.to_int().to_string())
    _ => ()
  }
  match entry.get("httpOnly") {
    Some(True) => parts.push("HttpOnly")
    _ => ()
  }
  match entry.get("secure") {
    Some(True) => parts.push("Secure")
    _ => ()
  }
  match entry.get("sameSite") {
    Some(String(raw_same_site)) => {
      let same_site = raw_same_site.to_lower()
      let mapped = if same_site == "none" {
        "None"
      } else if same_site == "lax" {
        "Lax"
      } else if same_site == "strict" {
        "Strict"
      } else {
        raw_same_site
      }
      parts.push("SameSite=" + mapped)
    }
    _ => ()
  }
  parts.join(";")
}

///|
pub fn synthetic_network_normalize_set_cookie_override_entry(
  entry : Map[String, Json],
  name : String,
  value_text : String,
  base_url : String,
  now_seconds : Int,
) -> Result[SyntheticNetworkSetCookieOverrideEntry, String] {
  if !synthetic_network_is_valid_header_name_token(name) {
    return Err("cookie name is invalid")
  }
  let mut domain = synthetic_network_cookie_domain_from_base_url(base_url)
  let mut domain_explicit = false
  let mut path = "/"
  let mut http_only = false
  let mut secure = false
  let mut same_site = "none"
  let mut expiry : Int? = None
  let header_entry : Map[String, Json] = {}
  header_entry["name"] = Json::string(name)
  header_entry["value"] = Json::string(value_text)

  match entry.get("domain") {
    Some(String(raw_domain)) => {
      let normalized_domain = synthetic_network_strip_leading_dot(
        raw_domain.trim().to_owned().to_lower(),
      )
      domain = normalized_domain
      domain_explicit = normalized_domain != ""
      header_entry["domain"] = Json::string(raw_domain)
    }
    Some(_) => return Err("cookie domain must be a string")
    None => ()
  }
  match entry.get("expiry") {
    Some(String(raw_expiry)) => {
      header_entry["expiry"] = Json::string(raw_expiry)
      let parsed_expiry = js_parse_cookie_expiry_seconds(raw_expiry)
      if parsed_expiry >= 0 {
        expiry = Some(parsed_expiry)
      }
    }
    Some(_) => return Err("cookie expiry must be a string")
    None => ()
  }
  match entry.get("path") {
    Some(String(raw_path)) => {
      path = raw_path
      header_entry["path"] = Json::string(raw_path)
    }
    Some(_) => return Err("cookie path must be a string")
    None => ()
  }
  match entry.get("sameSite") {
    Some(String(raw_same_site)) => {
      let lowered_same_site = raw_same_site.to_lower()
      if lowered_same_site != "none" &&
        lowered_same_site != "lax" &&
        lowered_same_site != "strict" {
        return Err("cookie sameSite is invalid")
      }
      same_site = lowered_same_site
      header_entry["sameSite"] = Json::string(lowered_same_site)
    }
    Some(_) => return Err("cookie sameSite must be a string")
    None => ()
  }
  match entry.get("httpOnly") {
    Some(True) => {
      http_only = true
      header_entry["httpOnly"] = Json::boolean(true)
    }
    Some(False) => header_entry["httpOnly"] = Json::boolean(false)
    Some(_) => return Err("cookie httpOnly must be a boolean")
    None => ()
  }
  match entry.get("secure") {
    Some(True) => {
      secure = true
      header_entry["secure"] = Json::boolean(true)
    }
    Some(False) => header_entry["secure"] = Json::boolean(false)
    Some(_) => return Err("cookie secure must be a boolean")
    None => ()
  }
  match entry.get("maxAge") {
    Some(raw_max_age) =>
      match
        synthetic_network_json_as_safe_int(
          raw_max_age, -9007199254740991.0, 9007199254740991.0,
        ) {
        Some(max_age) => {
          header_entry["maxAge"] = Json::number(max_age.to_double())
          expiry = Some(now_seconds + max_age)
        }
        None => return Err("cookie maxAge must be an integer")
      }
    None => ()
  }

  Ok({
    set_cookie_header_value: synthetic_network_format_set_cookie_header(
      header_entry,
    ),
    cookie: {
      name,
      value_text,
      domain,
      domain_explicit,
      path,
      http_only,
      secure,
      same_site,
      expiry,
    },
  })
}

///|
pub fn synthetic_network_normalize_request_cookie_entries(
  raw_cookies : Json?,
) -> Result[SyntheticNetworkCookieOverride, String] {
  match raw_cookies {
    None => Ok({ enabled: false, cookies: [] })
    Some(Null) => Ok({ enabled: true, cookies: [] })
    Some(Array(items)) => {
      let cookies : Array[Json] = []
      for item in items {
        let map = match item {
          Object(map) => map
          _ => return Err("requestCookies entry must be an object")
        }
        let name = match map.get("name") {
          Some(String(name)) => name
          _ => return Err("requestCookies.name must be a string")
        }
        let value = match map.get("value") {
          Some(Object(value_map)) => {
            let value_type = match value_map.get("type") {
              Some(String(value_type)) => value_type
              _ => return Err("requestCookies.value.type must be a string")
            }
            let payload = match value_map.get("value") {
              Some(String(payload)) => payload
              _ => return Err("requestCookies.value.value must be a string")
            }
            if value_type != "string" {
              return Err("requestCookies.value.type is invalid")
            }
            Json::object({
              "type": Json::string("string"),
              "value": Json::string(payload),
            })
          }
          _ => return Err("requestCookies.value must be an object")
        }
        cookies.push(
          Json::object({ "name": Json::string(name), "value": value }),
        )
      }
      Ok({ enabled: true, cookies })
    }
    Some(_) => Err("requestCookies must be an array")
  }
}

///|
fn synthetic_network_cookie_domain_from_base_url(base_url : String) -> String {
  match canonicalize_network_http_url(base_url) {
    Some(parts) => parts.host
    None =>
      if base_url.has_prefix("data:") {
        synthetic_network_inline_cookie_host(base_url).to_lower()
      } else {
        "localhost"
      }
  }
}

///|
fn synthetic_network_inline_cookie_host(url : String) -> String {
  let domain_value = extract_url_fragment_param(url, "domain").unwrap_or("")
  let base_host = if domain_value == "" || domain_value == "default" {
    "localhost"
  } else if domain_value == "alt" {
    "alt.localhost"
  } else if domain_value.contains(".") || domain_value.contains(":") {
    domain_value
  } else {
    domain_value + ".localhost"
  }
  let subdomain = extract_url_fragment_param(url, "subdomain").unwrap_or("")
  if subdomain == "" {
    base_host
  } else {
    subdomain + "." + base_host
  }
}

///|
fn extract_url_fragment_param(url : String, key : String) -> String? {
  match find_substring(url, "#", 0) {
    Some(hash_idx) => {
      let start = hash_idx + 1
      if start >= url.length() {
        return None
      }
      let fragment = url.unsafe_substring(start~, end=url.length())
      for part in fragment.split("&") {
        let raw = part.to_owned()
        if raw.length() == 0 {
          continue
        }
        match find_substring(raw, "=", 0) {
          Some(eq_idx) => {
            let part_key = raw.unsafe_substring(start=0, end=eq_idx)
            if part_key == key {
              let value_start = eq_idx + 1
              if value_start <= raw.length() {
                return Some(
                  raw.unsafe_substring(start=value_start, end=raw.length()),
                )
              }
              return Some("")
            }
          }
          None => if raw == key { return Some("") }
        }
      }
      None
    }
    None => None
  }
}

///|
fn synthetic_network_strip_leading_dot(value : String) -> String {
  if value.has_prefix(".") {
    network_slice_from(value, 1)
  } else {
    value
  }
}

///|
fn synthetic_network_parse_cookie_text(
  cookie_text : String,
  base_url : String,
  default_path : String,
  default_same_site : String,
  now_seconds : Int,
  require_valid_name : Bool,
) -> SyntheticNetworkCookie? {
  let attributes : Array[String] = []
  let mut first : String? = None
  for raw_part in cookie_text.split(";") {
    let part = raw_part.trim().to_owned()
    match first {
      Some(_) => attributes.push(part)
      None => first = Some(part)
    }
  }
  let first = match first {
    Some(first) => first
    None => return None
  }
  match find_substring(first, "=", 0) {
    Some(eq_idx) => {
      let name = network_slice(first, 0, eq_idx).trim().to_owned()
      let value_start = eq_idx + 1
      let value = if value_start <= first.length() {
        network_slice_from(first, value_start).trim().to_owned()
      } else {
        ""
      }
      if require_valid_name &&
        (name == "" || !synthetic_network_is_valid_header_name_token(name)) {
        return None
      }
      let mut domain = synthetic_network_cookie_domain_from_base_url(base_url)
      let mut domain_explicit = false
      let mut path = default_path
      let mut http_only = false
      let mut secure = false
      let mut same_site = default_same_site
      let mut expiry : Int? = None

      for raw_attr in attributes {
        let attr = raw_attr.trim().to_owned()
        if attr == "" {
          continue
        }
        let (attr_name, attr_value) = match find_substring(attr, "=", 0) {
          Some(attr_eq_idx) => {
            let attr_value_start = attr_eq_idx + 1
            (
              network_slice(attr, 0, attr_eq_idx).trim().to_owned().to_lower(),
              if attr_value_start <= attr.length() {
                network_slice_from(attr, attr_value_start).trim().to_owned()
              } else {
                ""
              },
            )
          }
          None => (attr.to_lower(), "")
        }
        if attr_name == "domain" {
          let normalized_domain = synthetic_network_strip_leading_dot(
            attr_value.trim().to_owned().to_lower(),
          )
          if normalized_domain != "" {
            domain = normalized_domain
            domain_explicit = true
          }
        } else if attr_name == "path" {
          if attr_value != "" {
            path = attr_value
          }
        } else if attr_name == "samesite" {
          let lowered = attr_value.to_lower()
          if lowered == "none" || lowered == "lax" || lowered == "strict" {
            same_site = lowered
          }
        } else if attr_name == "max-age" {
          let parsed_max_age : Int? = Some(@string.parse_int(attr_value)) catch {
            _ => None
          }
          match parsed_max_age {
            Some(parsed_max_age) => expiry = Some(now_seconds + parsed_max_age)
            None => ()
          }
        } else if attr_name == "expires" {
          let parsed_expiry = js_parse_cookie_expiry_seconds(attr_value)
          if parsed_expiry >= 0 {
            expiry = Some(parsed_expiry)
          }
        } else if attr_name == "httponly" {
          http_only = true
        } else if attr_name == "secure" {
          secure = true
        }
      }

      Some({
        name,
        value_text: value,
        domain,
        domain_explicit,
        path,
        http_only,
        secure,
        same_site,
        expiry,
      })
    }
    None => None
  }
}

///|
fn network_slice(value : String, start : Int, end : Int) -> String {
  value.unsafe_substring(start~, end~)
}

///|
fn network_slice_from(value : String, start : Int) -> String {
  if start > value.length() {
    ""
  } else {
    value.unsafe_substring(start~, end=value.length())
  }
}

///|
fn synthetic_network_json_as_safe_int(
  value : Json,
  min : Double,
  max : Double,
) -> Int? {
  match value {
    Number(n, ..) =>
      if n == n.to_int64().to_double() && n >= min && n <= max {
        Some(n.to_int64().to_int())
      } else {
        None
      }
    _ => None
  }
}

///|
fn is_ascii_case_insensitive_equal(lhs : String, rhs : String) -> Bool {
  if lhs.length() != rhs.length() {
    return false
  }
  let lhs_chars = lhs.to_array()
  let rhs_chars = rhs.to_array()
  for i = 0; i < lhs_chars.length(); i = i + 1 {
    let l = lhs_chars[i]
    let r = rhs_chars[i]
    if l == r {
      continue
    }
    let l_is_upper = l >= 'A' && l <= 'Z'
    let r_is_upper = r >= 'A' && r <= 'Z'
    if l_is_upper && l.to_int() + 32 == r.to_int() {
      continue
    }
    if r_is_upper && r.to_int() + 32 == l.to_int() {
      continue
    }
    return false
  }
  true
}