// ============================================================================
// 发送与自动跟随重定向:一次「跳」的完整流程——拼请求(含压缩声明与
// cookie 注入)→ 发送 → 收 Set-Cookie → 需要时跟下一跳。
//
// 拦截器**不在**这一层:每跟随一跳就调一次 `transport.send`,把拦截器挂进来
// 会让它每跳重跑一遍(认证头重复注入、重试被放大成「跳数 × 重试次数」)。
// 两段拦截器都落在 `Client::request` 与两个流式入口里,整条重定向链只跑一次。
// ============================================================================

///|
/// 这次请求往 `Accept-Encoding` 上声明什么(`None` = 不声明,交给传输实现决定)。
///
/// 一句话版规则:**谁解压谁声明**,谁都不解压时用 `identity` 把压缩挡住。
/// - 缓冲路径(`Client::request`):响应体由库读全、也由库解压,所以由库声明
///   `gzip, identity`;
/// - 流式路径(`stream` / `sse`):流式解压由底层实现透明完成——它见请求里没有
///   `Accept-Encoding` 就会自己补上并在响应头声明 gzip 时解压,所以这里不声明,
///   把决定权留给它(见 `docs/15-response-compression.md`);
/// - `decompress: false`:谁都不解压,于是声明 `identity` 让服务端别压缩
///   (也让底层不介入)。
///
/// 用户自己设置过 `Accept-Encoding` 时这段声明不生效(补默认头走
/// `set_if_absent`,见 `@util.build_prepared_request`):流式路径那时交出去的就是
/// 压缩字节(头也留着,自描述);缓冲路径是例外——它的承诺是「交出去的体一定解压」,
/// 所以只要响应头声明 gzip,读到的全量字节照样解(`decode_response_body`),
/// 无论那次声明是谁写的。
fn declared_encoding(config : Config, buffered : Bool) -> String? {
  if config.decompress.unwrap_or(true) {
    if buffered {
      Some("gzip, identity")
    } else {
      None
    }
  } else {
    Some("identity")
  }
}

///|
/// 拼请求:调 `@util.build_prepared_request`,把「这份配置拼不出可发送的请求」
/// 翻译成对外错误。
///
/// 为什么这层翻译要单独有个函数:纯函数用 `None` 表达失败(util 不认识
/// `HttpError`,也拿不到 `ErrorCode`),错误码与文案只能由编排层决定。
/// `request` 与 `open_stream` 两条入口都走它,免得同一份文案写两遍。
///
/// 两种失败原因分别报:**代理配置缺 host** 与**请求缺 url**。
/// 两者都是 `ERR_INVALID_URL`(都属于「地址不可用」),但修法完全不同,
/// 文案上不能混为一谈——代理那条判在调 util 之前,正是为了能分开说。
fn prepare_request(
  config : Config,
  signal : AbortSignal?,
  buffered : Bool,
) -> @transport.PreparedRequest raise HttpError {
  match config.proxy {
    Some(proxy) if !proxy.is_usable() =>
      raise make_error(
        "代理配置缺少 host:请用 with_proxy 给出代理服务器地址",
        ErrorCode::InvalidUrl,
        config,
        None,
      )
    _ => ()
  }
  let accept_encoding = declared_encoding(config, buffered)
  match @util.build_prepared_request(config, signal?, accept_encoding?) {
    Some(prepared) => prepared
    None =>
      raise make_error(
        "请求缺少 url:请在配置里提供 url 或 base_url",
        ErrorCode::InvalidUrl,
        config,
        None,
      )
  }
}

///|
/// 挂了 cookie 罐时按请求 URL 算出该带的 `Cookie` 头并**补**到请求上。
///
/// 走 `set_if_absent`:用户(或请求拦截器)显式设置的 `Cookie` 头永远优先,
/// 与 Content-Type / Authorization / Accept-Encoding 的补默认头口径一致。
/// 没挂罐、或 jar 认为这跳没有该带的 cookie 时原样返回。
fn Client::attach_cookie(
  self : Client,
  prepared : @transport.PreparedRequest,
) -> @transport.PreparedRequest {
  match self.cookie_jar {
    None => prepared
    Some(jar) =>
      match jar.cookie_header(prepared.url) {
        None => prepared
        Some(value) =>
          {
            ..prepared,
            headers: prepared.headers.set_if_absent("Cookie", value),
          }
      }
  }
}

///|
/// 把一跳响应里的 Set-Cookie 全量喂给 cookie 罐(没挂罐时什么都不做)。
///
/// 放在**每跳 `send` 返回之后**:响应头到手即可入库,不必等响应体读完——
/// 登录流的中途 3xx 一样会种 cookie,读体失败也不该丢掉已到手的会话状态。
fn Client::capture_cookies(
  self : Client,
  url : String,
  set_cookies : Array[String],
) -> Unit {
  match self.cookie_jar {
    None => ()
    Some(jar) =>
      for set_cookie in set_cookies {
        jar.store(url, set_cookie)
      }
  }
}

///|
/// 发送一次请求,需要时自动跟随重定向;返回**最后一跳**的原始响应与产出它的配置。
///
/// 分工:跟不跟、下一跳长什么样是纯逻辑(`@config.Config::next_redirect`,
/// 规则对齐 follow-redirects);这里只做那层做不到的四件事:
/// 1. **计数与上限**:最多跟 `max_redirects` 跳,第 `max_redirects + 1` 个
///    重定向就抛 `TooManyRedirects`;
/// 2. **释放连接**:跟随之前先关掉 3xx 的响应体——那段正文不要了,留着不读
///    会漏一条没人管的连接(等价 follow-redirects 的 `response.destroy()`);
/// 3. **每跳的错误现场**:传输失败时用**这一跳**的配置翻译错误,
///    与单跳时的规则一致;
/// 4. **每跳的 cookie**:挂了 cookie 罐时,发出前按这一跳的 URL 注入
///    `Cookie`(显式头优先),返回后把这一跳的 `Set-Cookie` 入库——跨域
///    重定向时 jar 按域匹配自然不再携带旧域的 cookie,与 `next_redirect`
///    剥显式 Cookie 头的规则互补(见 `docs/19-cookies.md`)。
///
/// `max_redirects <= 0` 时完全不跟随:3xx 原样返回,由状态码校验判定成败
/// ——与 axios 的 `maxRedirects: 0` 一致(那种配置下它连 follow-redirects 都不进)。
///
/// 上限未设置时按 5 兜底:内置默认值里本来就有它,正常路径上合并结果总是 `Some`;
/// 这里的兜底是留给「绕过 `defaults()` 直接用 util 层」的调用方的,
/// 数值取 axios 请求配置文档里的默认值 5。
///
/// 与 axios 的一处有意不同:每一跳各自受 `timeout` 约束(与既有的「建连到响应头
/// 整体一个时限、响应体每次读取一个时限」口径一致),所以整条链的最坏耗时是
/// 「上限 × timeout」;axios 是整条链共用一个计时器。
///
/// `buffered` 只用来决定请求头上的压缩声明(每跳都要重新拼请求,所以每跳都带上):
/// 值为 `true` 表示这次调用的响应体会被库读全,因此由库声明、也由库解压,
/// 见 `declared_encoding` 与 `docs/15-response-compression.md`。
async fn Client::send_following_redirects(
  self : Client,
  merged : Config,
  signal : AbortSignal?,
  buffered : Bool,
) -> (@transport.RawResponse, Config) raise HttpError {
  let limit = merged.max_redirects.unwrap_or(5)
  let mut config = merged
  let mut prepared = self.attach_cookie(
    prepare_request(config, signal, buffered),
  )
  // 发送:这一步失败说明响应头还没到手,错误里没有响应可挂。
  let mut raw = self.transport.send(prepared) catch {
    error => raise transport_error(error, config, None)
  }
  self.capture_cookies(prepared.url, raw.set_cookies)
  // 不跟随:3xx 原样交出去(默认的 2xx 校验会把它判成失败)。
  if limit <= 0 {
    return (raw, config)
  }
  let mut followed = 0
  // 循环条件写成变量而不是 `while true`:函数要返回最后那一跳的响应与配置,
  // 用条件退出才能让出口只有末尾那一处。
  let mut following = true
  while following {
    match
      config.next_redirect(
        prepared.url,
        raw.status,
        raw.headers.get("Location"),
      ) {
      // 不是重定向(非 3xx / 没有 Location / Location 解析不出地址)就跟完了。
      None => following = false
      Some(next) => {
        // 预算用完了重定向又来了一个:把最后这个 3xx 挂到错误上再抛。
        if followed >= limit {
          let (body, _failure) = raw.body.read_all_partial()
          raise too_many_redirects_error(
            limit,
            config,
            Some(
              build_response(
                raw.status,
                raw.status_text,
                raw.headers,
                body,
                config,
              ),
            ),
          )
        }
        // 流式请求体只在首跳有效(docs/20):`next_redirect` 没把这一跳降级成
        // GET,意味着下一跳要**原样重发请求体**——流已随首跳写完耗尽,
        // 重放只会静默发出空体。降级成 GET 的跳转(303、301/302 上的 POST)
        // 在 `next_redirect` 里就已把 data 清掉,不会走到这里。
        if next.has_stream_body() {
          let (body, _failure) = raw.body.read_all_partial()
          raise stream_replay_error(
            raw.status,
            config,
            Some(
              build_response(
                raw.status,
                raw.status_text,
                raw.headers,
                body,
                config,
              ),
            ),
          )
        }
        // 先释放这一跳的连接再发下一跳,否则每跟一次都漏一条连接。
        raw.body.close()
        followed = followed + 1
        config = next
        // 信号跟着每一跳走:取消落在两跳之间时,由传输层进入作用域时那次
        // `aborted()` 检查挡住(见 `docs/12-cancellation.md` 的「两条兜底」)。
        prepared = self.attach_cookie(prepare_request(config, signal, buffered))
        raw = self.transport.send(prepared) catch {
          error => raise transport_error(error, config, None)
        }
        self.capture_cookies(prepared.url, raw.set_cookies)
      }
    }
  }
  (raw, config)
}