// ============================================================================
// AGENTS.md RL-04 例外声明
//
// 本文件是拦截器的**唯一**落点:注册(含具名)、按名移除与三条链的驱动都在这里,
// 名字唯一性这类校验也收在此处——`client.mbt` 只消费一份已经自洽的 `Interceptors`
// 值,不参与注册或移除的任何一环。加入具名注册与按名移除后文件超过 300 行,按
// RL-04 为「根包文件」开出的例外处理——上限 1000 行。也不拆成 `interceptors/`
// 子包:`Response` / `HttpError` 定义在根包,反向引用会成环。
//
// 拦截器:请求侧(改配置 / 救错)与响应侧(改响应 / 救错)的四段式
//
// 对应 axios 的 `interceptors.request` / `interceptors.response`,顺序规则逐条对齐:
// 请求侧的链**后注册先跑**(LIFO)、响应侧**先注册先跑**(FIFO)。与 axios 的一处
// 有意差异是**错误按来源分流**:请求阶段的失败(被拦下、连不上、超时、被取消、
// 重定向超限、读响应体失败)只走请求侧的错误处理器,响应侧的错误处理器**只**接
// 「请求已经成功、但状态码没通过校验(默认非 2xx)」。两条错误链的落点、顺序与
// 「为什么与 axios 不一样」见 `docs/11-interceptors.md`。
//
// 为什么挂在 `Client`(而不是像 `params_serializer` 那样进 `Config`):`Response`
// 与 `HttpError` 定义在根包,`config` 包反向引用会成环;而且 axios 的
// `interceptors` 本来就是**实例级**(`axios.interceptors` / `instance.interceptors`),
// 不是请求级——挂在实例上才是对齐。
//
// 文件内的分区(顺序无关,用区块注释标出):
//   1. 四个函数类型   —— 拦截器 / 错误处理器的契约
//   2. Interceptors   —— 注册、同名替换与按名移除(链式、值语义)
//   3. 三条链的驱动   —— 顺序与错误流转(包私有,由 `client.mbt` 调)
// ============================================================================

// ---------------------------------------------------------------------------
// 1. 四个函数类型 —— 拦截器与错误处理器的契约
// ---------------------------------------------------------------------------

///|
/// 请求拦截器:拿到**合并后**的配置,返回(可能改写过的)配置。
///
/// 拿到的已经是「实例默认值 + 本次请求配置」的合并结果(与 axios 一致),所以
/// 可以直接改地址、加头、换请求体,甚至换方法。抛错表示**拦下这次请求**:请求
/// 不会发出,错误按来源分流交给**请求侧**的错误处理器
/// (`HttpError::new` 就是给这种中止准备的,见 `http_error.mbt`)。
///
/// 类型带 `async` 与 `raise HttpError` 不是装饰:`raise` 让拦截器能中止请求,
/// `async` 让「发请求前先去别处取一个 token」这类事写在同一处。代价是**具名同步
/// 函数不能直接传进来**——MoonBit 的效果推断只认箭头语法,写成 `config => ...`
/// 或用 `async fn` 显式标注即可。
pub type RequestInterceptor = async (Config) -> Config raise HttpError

///|
/// 请求侧的错误处理器,对应 axios 里 `interceptors.request.use(f, r)` 的第二个
/// 参数(但覆盖面不同:axios 的请求侧 `r` 接不到派发失败,这里接得到)。
///
/// 拿到的是**请求阶段**的失败:被请求拦截器拦下、配置拼不出可发送的请求(缺 url、
/// 代理缺 host)、连不上、超时、被取消、重定向超限、读响应体的中途失败。
/// 返回一个 `Response` 表示**这次失败就地救回来**,它会作为 `request` 的结果返回;
/// `raise error` 表示不处理、交给更早注册的错误处理器(请求侧错误链是**后注册
/// 先跑**),都没有接住就抛给调用方——与 axios 的 `Promise.reject(error)` 等价。
///
/// 救回的响应**不再经过响应侧链**:响应侧管的是「服务端这次返回的响应」,而救回的
/// 多半是缓存里存的、或本地兜底合成的,两段链各管一段。要让它也走一遍响应改写,
/// 在处理器里自己调一次改写函数即可。
///
/// 重试写在处理器体内:拿 `error.config()` 用另一个不带这层拦截器的实例再发一次
/// (与响应侧那份配方一样),见 `docs/11-interceptors.md`。
///
/// 签名与 `ResponseErrorHandler` 完全相同,分开命名是为了让两段链各自自解释、
/// 也让 `.mbti` 一眼看出这个处理器属于哪一侧。
pub type RequestErrorHandler = async (HttpError) -> Response raise HttpError

///|
/// 响应拦截器(正常路径):拿到最终的 `Response`,返回(可能改写过的)响应。
///
/// 只在 `Client::request` 上跑:`stream` / `sse` 的「响应」是还没读的字节流,
/// 改写与重试都没有明确语义(读法由入口决定是本项目已有的口径)。改写响应用
/// `Response::with_status` / `with_headers` / `with_body` / `with_text` / `with_json`;
/// 「解析 → 处理 → 写回」的配方(`json()` 抛 `@json.ParseError`,要用 `try ... catch`
/// 收掉)见 `docs/11-interceptors.md`。
pub type ResponseInterceptor = async (Response) -> Response raise HttpError

///|
/// 响应侧的错误处理器,对应 axios 里 `interceptors.response.use(f, r)` 的第二个参数。
///
/// **只接「请求已经成功、但状态码没通过校验」(默认只放行 2xx)**:这时
/// `HttpError::response()` 上有完整响应,状态码、响应头、错误正文都在手里,可以就地
/// 降级成正常响应、改写成统一形状,或重发一次。请求阶段的失败(连不上、超时、
/// 被取消、被请求拦截器拦下)**不进这里**,它们由 `RequestErrorHandler` 接
/// ——这条分流是本项目与 axios 的 promise 链有意不同的地方,理由见
/// `docs/11-interceptors.md`。
///
/// 返回一个 `Response` 表示**这个错误已经处理掉**,它会作为 `request` 的结果
/// 返回;`raise error` 表示不处理、继续往外传——与 axios 的
/// `Promise.reject(error)` 等价。重试就是在这里再发一次请求。
pub type ResponseErrorHandler = async (HttpError) -> Response raise HttpError

// ---------------------------------------------------------------------------
// 2. Interceptors —— 注册、同名替换与按名移除
//
// 「同一侧内名字唯一」是这一节维护的**不变量**:带名字的注册要么追加、要么原地换掉
// 同名那条,于是 `remove_*` 撤哪个没有歧义。查找规则与辅助函数都是私有的,不外泄。
// ---------------------------------------------------------------------------

///|
/// 一对请求处理器。**私有**(不标 `priv` 会出现在 `.mbti` 里):注册时成对给
/// (对齐 axios 的 `use(f, r)`),两个处理器的**相对位置**才是语义所在——
/// `on_rejected` 接住的是「被本拦截器或更晚注册的拦截器拦下」以及派发阶段的失败,
/// 与更早注册的处理器无关。
priv struct RequestPair {
  on_fulfilled : RequestInterceptor
  on_rejected : RequestErrorHandler
  /// 注册时给的名字,`None` = 无名注册(事后撤不下来)
  name : String?
}

///|
/// 一对响应处理器。**私有**(不标 `priv` 会出现在 `.mbti` 里):注册时成对给
/// (对齐 axios 的 `use(f, r)`),两个处理器的**相对位置**才是语义所在——
/// 错误处理器接住的是「排在它前面」的一切抛出的错误(状态码校验失败、更早注册的
/// 处理器),**不包括同一对里正常处理器抛出的错误**:那个由更晚注册的错误处理
/// 器接(promise 链里 `then(f, r)` 的 `r` 只处理「进入这一对之前」已是失败的
/// 情况,`src/interceptor_test.mbt` 有一条用例钉着)。
priv struct ResponsePair {
  on_fulfilled : ResponseInterceptor
  on_rejected : ResponseErrorHandler
  /// 注册时给的名字,`None` = 无名注册(事后撤不下来)
  name : String?
}

///|
/// 拦截器的集合,对应 axios 实例上的 `interceptors`。
///
/// 链式构建、**值语义**:`use_*` 与 `remove_*` 都不改自己,而是返回改动后的新值
/// (与 `FormData::append_text` 同一口径——数组是共享的,就地改会让别的集合跟着变)。
/// 传给 `Client::new` 之后视作冻结:实例拿的是当时那份值,之后的注册与移除都不会
/// 影响到它——所以要给某个实例换链,就是换一份值重建(没有实例级的读写口)。
///
/// 注册时可以给一条起名(`use_request(f, name="auth")`):名字让这条**可寻址**
/// ——同名再注册替换原位、`remove_request(name)` 按名撤下,用途是「一份共享的基础链
/// 按客户端裁剪」,这是 `Client::create` 整份继承拦截器时唯一能表达「少挂一条」的
/// 手段。无名注册的撤不下来,撤链要走名字。
///
/// ```moonbit nocheck
/// let client = @moonhttp.Client::new(
///   interceptors=@moonhttp.Interceptors::new()
///     .use_request(config => config.with_header("X-Token", token), name="auth")
///     .use_response(response => response.with_text(unwrap(response.text()))),
/// )
///
/// // 共享一份基础链,按客户端裁剪:不必把链再抄一遍,基础链演进时也不会漂
///
/// let base = @moonhttp.Interceptors::new()
///   .use_request(config => config.with_header("X-Token", token), name="auth")
///   .use_response(rewrite_json, name="rewrite")
///
/// let trimmed = base.remove_response("rewrite").unwrap()
/// ```
pub struct Interceptors {
  priv requests : Array[RequestPair]
  priv responses : Array[ResponsePair]
}

///|
/// 空的拦截器集合:一条链都不挂。
pub fn Interceptors::new() -> Interceptors {
  { requests: [], responses: [], }
}

///|
/// 按名字找请求处理器在链上的下标;没有这个名字返回 `None`。
///
/// 私有:名字的唯一性与查找规则都收在本文件内,包外只需要「注册」与「按名移除」
/// 两个动作,不需要认识下标(下标在条件注册下会漂,这正是寻址走名字的原因)。
/// 名字按原样精确匹配——不做 trim、不折叠大小写:拦截器的名字永不上线,
/// 规范化只会带来「两个写法指向同一个键」这种看不见的行为。
fn request_index_of(requests : Array[RequestPair], name : String) -> Int? {
  for i = 0; i < requests.length(); i = i + 1 {
    match requests[i].name {
      Some(existing) => if existing == name { return Some(i) }
      None => ()
    }
  }
  None
}

///|
/// 按名字找响应处理器在链上的下标;没有这个名字返回 `None`。与请求侧同规则,
/// 两侧各扫各的数组(命名空间独立)。
fn response_index_of(responses : Array[ResponsePair], name : String) -> Int? {
  for i = 0; i < responses.length(); i = i + 1 {
    match responses[i].name {
      Some(existing) => if existing == name { return Some(i) }
      None => ()
    }
  }
  None
}

///|
/// 注册一对请求处理器:正常处理器**后注册先跑**(axios 的请求拦截器是 LIFO),
/// 错误处理器与它同向(请求侧的两条链只有一个方向,见 `docs/11-interceptors.md`)。
///
/// `on_rejected` 省略表示「这个拦截器不管错误」,错误继续交给更早注册的错误处理器
/// (与 axios 里不写第二个参数同义)。
///
/// `name` 省略 = 无名注册:链尾追加,事后撤不下来(要能撤就得现在起名)。给了名字
/// 则这条**可寻址**:同名再注册**替换原位**——位置不变,LIFO 顺序照旧,所以同一侧
/// 内不存在同名两条;`remove_request(name)` 按名撤下。名字只在请求侧内唯一,与响应侧
/// 互不相干。
///
/// 两个可选参数调用时都得写标签(可选参数不能按位置传,实测):
/// `.use_request(f, on_rejected=r)` / `.use_request(f, name="auth")`。
pub fn Interceptors::use_request(
  self : Interceptors,
  on_request : RequestInterceptor,
  on_rejected? : RequestErrorHandler,
  name? : StringView,
) -> Interceptors {
  let on_rejected = match on_rejected {
    Some(handler) => handler
    None => error => raise error
  }
  // copy 后再改:数组可能属于别的集合(比如一份共享的基础链),就地改会让它跟着变。
  let requests = self.requests.copy()
  match name {
    None =>
      requests.push({ on_fulfilled: on_request, on_rejected, name: None, })
    Some(value) => {
      let key = value.to_owned()
      // 显式标注:两个 pair 的字段名同形,字面量单看无法判断是哪一种。
      let pair : RequestPair = {
        on_fulfilled: on_request,
        on_rejected,
        name: Some(key),
      }
      match request_index_of(requests, key) {
        Some(index) => requests[index] = pair
        None => requests.push(pair)
      }
    }
  }
  { requests, responses: self.responses, }
}

///|
/// 注册一对响应处理器:**先注册先跑**(axios 的响应拦截器是 FIFO)。
///
/// `on_rejected` 省略表示「这个拦截器不管错误」,错误继续交给更晚注册的错误
/// 处理器(与 axios 里不写第二个参数同义)。
///
/// `name` 与请求侧同规则:省略 = 无名(撤不下来,FIFO 顺序里照常跑),给了名字则
/// 同名替换原位、可用 `remove_response(name)` 撤下;名字只在响应侧内唯一。
pub fn Interceptors::use_response(
  self : Interceptors,
  on_fulfilled : ResponseInterceptor,
  on_rejected? : ResponseErrorHandler,
  name? : StringView,
) -> Interceptors {
  let on_rejected = match on_rejected {
    Some(handler) => handler
    None => error => raise error
  }
  // copy 后再改:数组可能属于别的集合,就地改会让它跟着变。
  let responses = self.responses.copy()
  match name {
    None => responses.push({ on_fulfilled, on_rejected, name: None, })
    Some(value) => {
      let key = value.to_owned()
      // 显式标注:两个 pair 的字段名同形,字面量单看无法判断是哪一种。
      let pair : ResponsePair = { on_fulfilled, on_rejected, name: Some(key), }
      match response_index_of(responses, key) {
        Some(index) => responses[index] = pair
        None => responses.push(pair)
      }
    }
  }
  { requests: self.requests, responses, }
}

///|
/// 按名撤下一个请求拦截器,返回**新的集合**;这个名字没注册过时返回 `None`。
///
/// 名字没对上就返回 `None`,不 panic,也不静默当成撤成功:拼错的名字必须立刻可见,
/// 否则「拦截器其实还在跑」是查不出来的那类 bug。`None` 时一个元素都不动,
/// 原集合可以照用。
///
/// 对应 axios 的 `eject(id)`,差别在寻址方式:axios 用 `use` 返回的数字 id,这里用注册
/// 时给的名字——下标在条件注册下会漂,名字不会。无名注册的那几条撤不下来,要撤就得在
/// 注册时起名;撤下只影响这份值,已经交给 `Client::new` 的实例不受影响。
pub fn Interceptors::remove_request(
  self : Interceptors,
  name : StringView,
) -> Interceptors? {
  let key = name.to_owned()
  match request_index_of(self.requests, key) {
    None => None
    Some(index) => {
      let requests = self.requests.copy()
      ignore(requests.remove(index))
      Some({ requests, responses: self.responses, })
    }
  }
}

///|
/// 按名撤下一个响应拦截器,返回**新的集合**;这个名字没注册过时返回 `None`。
/// 语义与 `remove_request` 相同,只是落在响应侧(FIFO 顺序里剩下的相对位置不变)。
pub fn Interceptors::remove_response(
  self : Interceptors,
  name : StringView,
) -> Interceptors? {
  let key = name.to_owned()
  match response_index_of(self.responses, key) {
    None => None
    Some(index) => {
      let responses = self.responses.copy()
      ignore(responses.remove(index))
      Some({ requests: self.requests, responses, })
    }
  }
}

// ---------------------------------------------------------------------------
// 3. 三条链的驱动 —— 顺序与错误流转
// ---------------------------------------------------------------------------

///|
/// 跑请求侧正常链:**后注册先跑**(LIFO,与 axios 一致),每个拦截器拿到的都是
/// 上一个(更晚注册的)拦截器交出来的配置。
///
/// 不抛错而是返回 `Result`:被拦下不是本函数的异常,而是「这次请求不发了」,
/// 按来源分流应当交给请求侧的错误处理器——交给谁由调用方决定(`request` 交给
/// `run_request_errors`,两个流式入口没有错误处理器,直接抛)。
/// 因此这里显式标 `noraise`:拦截器抛出的错误全部收进了 `Err`,调用方不必
/// (也不该)在自己的错误类型里为它留位置。
async fn Interceptors::run_request(
  self : Interceptors,
  config : Config,
) -> Result[Config, HttpError] noraise {
  let mut outcome : Result[Config, HttpError] = Ok(config)
  for i = self.requests.length() - 1; i >= 0; i = i - 1 {
    // 字段里的函数值要先绑到局部变量再调:`pair.on_fulfilled(x)` 会被当成
    // 「在 `RequestPair` 上找一个叫 on_fulfilled 的方法」。
    let on_request = self.requests[i].on_fulfilled
    outcome = match outcome {
      // 正常路径:交出配置;拦截器抛错就转成「已拦下」。
      Ok(current) => Ok(on_request(current)) catch { error => Err(error) }
      // 已经被拦下:正常处理器全部跳过,错误留给错误链。
      Err(error) => Err(error)
    }
  }
  outcome
}

///|
/// 跑请求侧错误链:**后注册先跑**(与请求侧正常链同向,理由见
/// `docs/11-interceptors.md`)。
///
/// 每个错误处理器拿到当前错误:返回 `Response` 表示就地救回,立刻结束(这次请求
/// 的结果就是它);`raise error` 表示不处理,把(可能是自己新造的)错误交给更早
/// 注册的处理器。全部没接住就抛最后那个错误——与 axios 的 promise 链一样,
/// 错误处理器可以「换一个错误继续往外传」。
async fn Interceptors::run_request_errors(
  self : Interceptors,
  error : HttpError,
) -> Response raise HttpError {
  let mut current = error
  let mut rescued : Response? = None
  for i = self.requests.length() - 1; i >= 0; i = i - 1 {
    // 救回来之后剩下的处理器不再打扰:链的意义就是「谁接住就谁说了算」。
    if rescued is None {
      let on_rejected = self.requests[i].on_rejected
      let outcome = Ok(on_rejected(current)) catch { e => Err(e) }
      match outcome {
        Ok(response) => rescued = Some(response)
        Err(next) => current = next
      }
    }
  }
  match rescued {
    Some(response) => response
    None => raise current
  }
}

///|
/// 跑响应侧链:**先注册先跑**(FIFO,与 axios 一致)。
///
/// 进来的 `outcome` 只有两种来源:通过校验的响应,或**状态码没通过校验**的错误
/// (请求阶段的失败不进这里,它们走 `run_request_errors`)。与 axios 的 promise
/// 链同构:`Ok` 走正常处理器、`Err` 走错误处理器;处理器自己抛出的错误落到
/// **更晚注册**的错误处理器手里,没人接住就抛给调用方。
/// 所以「状态码失败的重试」写在哪里是有讲究的:越早注册的错误处理器越先看到错误。
async fn Interceptors::run_response(
  self : Interceptors,
  outcome : Result[Response, HttpError],
) -> Response raise HttpError {
  let mut state = outcome
  for pair in self.responses {
    let on_fulfilled = pair.on_fulfilled
    let on_rejected = pair.on_rejected
    state = match state {
      Ok(response) => Ok(on_fulfilled(response)) catch { error => Err(error) }
      Err(error) => Ok(on_rejected(error)) catch { error => Err(error) }
    }
  }
  match state {
    Ok(response) => response
    Err(error) => raise error
  }
}