FilterUtility::finalTimeout(const RouteEntry& route, Http::RequestHeaderMap& request_headers,
                            bool insert_envoy_expected_request_timeout_ms, bool grpc_request,
                            bool per_try_timeout_hedging_enabled,
                            bool respect_expected_rq_timeout) {
  // See if there is a user supplied timeout in a request header. If there is we take that.
  // Otherwise if the request is gRPC and a maximum gRPC timeout is configured we use the timeout
  // in the gRPC headers (or infinity when gRPC headers have no timeout), but cap that timeout to
  // the configured maximum gRPC timeout (which may also be infinity, represented by a 0 value),
  // or the default from the route config otherwise.
  TimeoutData timeout;
  if (!route.usingNewTimeouts()) {
    if (grpc_request && route.maxGrpcTimeout()) {
      const std::chrono::milliseconds max_grpc_timeout = route.maxGrpcTimeout().value();
      auto header_timeout = Grpc::Common::getGrpcTimeout(request_headers);
      std::chrono::milliseconds grpc_timeout =
          header_timeout ? header_timeout.value() : std::chrono::milliseconds(0);
      if (route.grpcTimeoutOffset()) {
        // We only apply the offset if it won't result in grpc_timeout hitting 0 or below, as
        // setting it to 0 means infinity and a negative timeout makes no sense.
        const auto offset = *route.grpcTimeoutOffset();
        if (offset < grpc_timeout) {
          grpc_timeout -= offset;
        }
      }

      // Cap gRPC timeout to the configured maximum considering that 0 means infinity.
      if (max_grpc_timeout != std::chrono::milliseconds(0) &&
          (grpc_timeout == std::chrono::milliseconds(0) || grpc_timeout > max_grpc_timeout)) {
        grpc_timeout = max_grpc_timeout;
      }
      timeout.global_timeout_ = grpc_timeout;
    } else {
      timeout.global_timeout_ = route.timeout();
    }
  }
  timeout.per_try_timeout_ = route.retryPolicy().perTryTimeout();
  timeout.per_try_idle_timeout_ = route.retryPolicy().perTryIdleTimeout();

  uint64_t header_timeout;

  if (respect_expected_rq_timeout) {
    // Check if there is timeout set by egress Envoy.
    // If present, use that value as route timeout and don't override
    // *x-envoy-expected-rq-timeout-ms* header. At this point *x-envoy-upstream-rq-timeout-ms*
    // header should have been sanitized by egress Envoy.
    const Http::HeaderEntry* header_expected_timeout_entry =
        request_headers.EnvoyExpectedRequestTimeoutMs();
    if (header_expected_timeout_entry) {
      trySetGlobalTimeout(*header_expected_timeout_entry, timeout);
    } else {
      const Http::HeaderEntry* header_timeout_entry =
          request_headers.EnvoyUpstreamRequestTimeoutMs();

      if (header_timeout_entry) {
        trySetGlobalTimeout(*header_timeout_entry, timeout);
        request_headers.removeEnvoyUpstreamRequestTimeoutMs();
      }
    }
  } else {
    const Http::HeaderEntry* header_timeout_entry = request_headers.EnvoyUpstreamRequestTimeoutMs();

    if (header_timeout_entry) {
      trySetGlobalTimeout(*header_timeout_entry, timeout);
      request_headers.removeEnvoyUpstreamRequestTimeoutMs();
    }
  }

  // See if there is a per try/retry timeout. If it's >= global we just ignore it.
  const absl::string_view per_try_timeout_entry =
      request_headers.getEnvoyUpstreamRequestPerTryTimeoutMsValue();
  if (!per_try_timeout_entry.empty()) {
    if (absl::SimpleAtoi(per_try_timeout_entry, &header_timeout)) {
      timeout.per_try_timeout_ = std::chrono::milliseconds(header_timeout);
    }
    request_headers.removeEnvoyUpstreamRequestPerTryTimeoutMs();
  }

  if (timeout.per_try_timeout_ >= timeout.global_timeout_ && timeout.global_timeout_.count() != 0) {
    timeout.per_try_timeout_ = std::chrono::milliseconds(0);
  }

  // See if there is any timeout to write in the expected timeout header.
  uint64_t expected_timeout = timeout.per_try_timeout_.count();
  // Use the global timeout if no per try timeout was specified or if we're
  // doing hedging when there are per try timeouts. Either of these scenarios
  // mean that the upstream server can use the full global timeout.
  if (per_try_timeout_hedging_enabled || expected_timeout == 0) {
    expected_timeout = timeout.global_timeout_.count();
  }

  if (insert_envoy_expected_request_timeout_ms && expected_timeout > 0) {
    request_headers.setEnvoyExpectedRequestTimeoutMs(expected_timeout);
  }

  // If we've configured max_grpc_timeout, override the grpc-timeout header with
  // the expected timeout. This ensures that the optional per try timeout is reflected
  // in grpc-timeout, ensuring that the upstream gRPC server is aware of the actual timeout.
  // If the expected timeout is 0 set no timeout, as Envoy treats 0 as infinite timeout.
  if (grpc_request && !route.usingNewTimeouts() && route.maxGrpcTimeout() &&
      expected_timeout != 0) {
    Grpc::Common::toGrpcTimeout(std::chrono::milliseconds(expected_timeout), request_headers);
  }

  return timeout;
}