///|
/// Endpoint location resolved without depending on an HTTP framework router.
pub(all) enum TusRoute {
  Collection
  Resource(String)
} derive(Debug, Eq)

///|
/// Validate generic request constraints, protocol version and endpoint path.
pub fn validate_request(
  request : TusRequest,
  config : TusConfig,
) -> Result[TusRoute, TusError] {
  match validate_config(config) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  match request.headers.validate(config.limits) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  if request.path.length() > config.limits.max_path_bytes {
    return Err(
      tus_error(
        InvalidPath,
        "TUS_PATH_TOO_LARGE",
        "request path exceeds the configured byte budget",
        status=414,
        expected=Some(config.limits.max_path_bytes.to_string()),
        actual=Some(request.path.length().to_string()),
      ),
    )
  }
  match request.http_method {
    Options => ()
    Post | Head | Patch | Other(_) =>
      match require_tus_version(request.headers) {
        Err(error) => return Err(error)
        Ok(_) => ()
      }
  }
  classify_path(request.path, config.collection_path)
}

///|
pub fn require_tus_version(headers : Headers) -> Result[Unit, TusError] {
  match headers.required_singleton("tus-resumable", MissingTusResumable) {
    Err(error) => Err(error)
    Ok(value) => {
      let actual = trim_ows(value)
      if actual == TUS_VERSION {
        Ok(())
      } else {
        Err(unsupported_version(actual))
      }
    }
  }
}

///|
/// Apply the core protocol's X-HTTP-Method-Override before routing. The
/// returned request keeps headers and body but exposes the effective method.
pub fn apply_method_override(
  request : TusRequest,
) -> Result[TusRequest, TusError] {
  match request.headers.singleton("x-http-method-override") {
    Err(error) => Err(error)
    Ok(None) => Ok(request)
    Ok(Some(value)) => {
      let effective = parse_http_method(trim_ows(value))
      match effective {
        Other(name) => Err(invalid_method(name))
        _ =>
          Ok({
            http_method: effective,
            path: request.path,
            headers: request.headers,
            body: request.body,
          })
      }
    }
  }
}

///|
pub fn classify_path(
  path : String,
  collection_path : String,
) -> Result[TusRoute, TusError] {
  if path == collection_path {
    return Ok(Collection)
  }
  let prefix = if collection_path == "/" { "/" } else { collection_path + "/" }
  if path.length() <= prefix.length() ||
    path[:prefix.length()].to_owned() != prefix {
    return Err(invalid_path(path))
  }
  let identifier = path[prefix.length():].to_owned()
  if !is_safe_identifier(identifier) {
    return Err(invalid_path(path))
  }
  Ok(Resource(identifier))
}

///|
pub fn resource_path(collection_path : String, identifier : String) -> String {
  if collection_path == "/" {
    "/" + identifier
  } else {
    collection_path + "/" + identifier
  }
}

///|
pub fn is_safe_identifier(identifier : String) -> Bool {
  if identifier.length() == 0 || identifier.length() > 128 {
    return false
  }
  for character in identifier {
    if !is_identifier_character(character) {
      return false
    }
  }
  true
}

///|
fn is_identifier_character(character : Char) -> Bool {
  if character >= 'a' && character <= 'z' {
    return true
  }
  if character >= 'A' && character <= 'Z' {
    return true
  }
  if character >= '0' && character <= '9' {
    return true
  }
  character == '-' || character == '_' || character == '.' || character == '~'
}

///|
pub fn validate_config(config : TusConfig) -> Result[Unit, TusError] {
  let path = config.collection_path
  if path.length() == 0 || path[0] != '/' {
    return Err(config_error("collection path must start with a slash"))
  }
  if path.length() > 1 && path[path.length() - 1] == '/' {
    return Err(config_error("collection path must not end with a slash"))
  }
  for character in path {
    let code = character.to_int()
    if code < 33 || code > 126 || character == '?' || character == '#' {
      return Err(config_error("collection path contains an unsafe character"))
    }
  }
  let limits = config.limits
  if limits.max_upload_size < 0L ||
    limits.max_patch_bytes < 0 ||
    limits.max_header_count <= 0 ||
    limits.max_header_bytes <= 0 ||
    limits.max_metadata_bytes < 0 ||
    limits.max_metadata_entries < 0 ||
    limits.max_path_bytes <= 0 ||
    limits.max_identifier_attempts <= 0 {
    return Err(
      config_error(
        "all size limits must be non-negative and structural limits positive",
      ),
    )
  }
  Ok(())
}

///|
fn config_error(message : String) -> TusError {
  tus_error(InternalInvariant, "TUS_CONFIG_INVALID", message, status=500)
}