// 请求取消(对应 axios 的 `cancelToken` / 原生 `AbortSignal`)。
//
// 为什么单独一个文件而不是留在 config.mbt:那里已经贴着 RL-04 的 300 行上限,
// 取消这块是能整体搬走的一部分(字段必须留在结构体里),与 `progress.mbt` /
// `proxy.mbt` 同一思路——文件按功能分。
//
// 这里只有**状态**,没有底层机制:token 记「取消没取消」以及一组回调,
// 真正打断一次请求的协程取消发生在 transport 包(那里才有 `@async`)。
// 这样取消的语义(谁能被取消、取消后读到什么)可以在 config 包里同步测试,
// 而 config 包始终不依赖 async 运行时。
//
// 契约细节(能打断什么、覆盖哪些入口、与 timeout / 重试的关系)见
// `docs/12-cancellation.md`。

///|
/// 取消状态。一次性的:从 `Pending` 走到 `Cancelled` 之后不再变化,
/// 取消理由也一起定格。
priv enum CancelState {
  Pending
  // 位置参数而不是 `message : String?`:MoonBit 的枚举载荷要带名字得写
  // `message~ : String?`,这里只有一个载荷,位置写法更短也够清楚。
  Cancelled(String?)
}

///|
/// 取消句柄:一个可以传给任意多次请求的「取消信号」。
///
/// 用法与 axios 的 `CancelToken` 一致,但**只有一个对象**——axios 把
/// 「传出去的 token」与「调用方手里的 source」分成两个,是为了避免拿到 token
/// 的一方误调用 `cancel`;在 MoonBit 里这层防护换来的是每次使用都多一个类型,
/// 而取消这件事本来就需要某个持有者来触发,二分的收益抵不上成本。
///
/// 几条刻意的语义:
/// - **一次性**:取消之后永远是取消状态,不能复用(与 axios 一致);
///   需要「一次请求一个信号」就每次 `CancelToken::new()`。
/// - **可共享**:同一个 token 可以挂到多个请求上,`cancel` 一次全部生效;
///   取消发生之后才挂上来的请求会**立刻失败**,不会「取消晚了一步就照常发出」。
/// - **幂等**:重复 `cancel` 只生效第一次,第二次连理由都不会改。
///
/// `cancel` 是同步函数且不抛错,所以可以从任何地方调用——包括进度回调里
/// (它是 `noraise` 的,得不到通知就只能靠这种同步调用)。落点见
/// `docs/12-cancellation.md` 的「取消的落点」。
pub struct CancelToken {
  priv mut state : CancelState
  /// 已登记的回调槽位;取消时整组取走再清空,`None` 表示该槽位已注销。
  priv mut handles : Array[(() -> Unit)?]
  /// 递增票号,`detach` 时用来认领槽位。
  ///
  /// 只增不减:取消之后数组会被清空,旧票号一定越界,`detach` 忽略即可。
  priv mut next_ticket : Int
}

///|
/// `attach` 在「已经取消」时返回的票号:这次登记没有槽位可注销。
const NO_TICKET : Int = -1

///|
/// 新建一个尚未取消的 token。
pub fn CancelToken::new() -> CancelToken {
  { state: CancelState::Pending, handles: [], next_ticket: 0, }
}

///|
/// 取消。`message` 会原样成为这次请求失败时的错误描述(`HttpError::message`);
/// 不传就用库的默认文案(见 `docs/04-errors.md`)。
///
/// 同步、不抛错、幂等:第一次之后的所有调用都是空操作。触发的回调按登记顺序
/// 同步执行,所以调用 `cancel` 的那条协程会等所有清理动作做完
/// (例如关闭响应体连接)才继续。
pub fn CancelToken::cancel(self : CancelToken, message? : String) -> Unit {
  match self.state {
    Cancelled(_) => ()
    Pending => {
      self.state = CancelState::Cancelled(message)
      // 先整组取走再逐个调用:回调里可能反过来注销自己——`ResponseBody::close`
      // 就会调用 `detach`,边遍历边改同一个数组会把注销写丢。
      let handles = self.handles
      self.handles = []
      for handle in handles {
        match handle {
          Some(handle) => handle()
          None => ()
        }
      }
    }
  }
}

///|
/// 是否已取消。
pub fn CancelToken::is_cancelled(self : CancelToken) -> Bool {
  match self.state {
    Pending => false
    Cancelled(_) => true
  }
}

///|
/// 取消时传入的 `message`;没取消、或取消时没给 message 时是 `None`。
///
/// 想区分「没取消」与「取消了但没给理由」用 `is_cancelled()` 配它,
/// 别用 `reason()` 的 `None` 当判断依据。
pub fn CancelToken::reason(self : CancelToken) -> String? {
  match self.state {
    Pending => None
    Cancelled(message) => message
  }
}

///|
/// 登记一个「取消时要做的事」,返回注销用的票号。
///
/// **这是给传输层用的内部机制**,不是使用者 API:一次请求在开始一段可被取消的
/// I/O 之前把自己的中断手段挂上来,结束时注销。使用者只该调 `cancel`。
///
/// 已经取消的 token 会**立刻**调用 `handle`,返回 `NO_TICKET`。这条不是顺手
/// 补的容错,而是取消语义的兜底:`cancel` 只通知得到当时登记过的句柄,而请求
/// 可能在两段 I/O 之间(重定向的两跳之间就是一个真实的时间窗)才去登记,
/// 这时它必须马上断掉,而不是照常把这跳发出去。
pub fn CancelToken::attach(
  self : CancelToken,
  handle : () -> Unit noraise,
) -> Int {
  match self.state {
    Cancelled(_) => {
      handle()
      NO_TICKET
    }
    Pending => {
      let ticket = self.next_ticket
      self.next_ticket = self.next_ticket + 1
      self.handles.push(Some(handle))
      ticket
    }
  }
}

///|
/// 注销一次登记。票号越界一律忽略——这既覆盖「重复注销」,也覆盖取消链路里
/// 的那次自注销(`cancel` 已经把数组清空,旧票号必然越界)。
pub fn CancelToken::detach(self : CancelToken, ticket : Int) -> Unit {
  if ticket >= 0 && ticket < self.handles.length() {
    self.handles[ticket] = None
  }
}

///|
/// 给这次请求挂上取消句柄,对应 axios 的 `cancelToken` / `signal`。
///
/// 与 axios 一样,token 可以来自请求级配置,也可以放在实例默认值上
/// (后者是「这个实例发出的请求共用一个取消信号」的用法)。
///
/// ```moonbit nocheck
/// let token = CancelToken::new()
/// let config = Config::new("/reports/big.csv")
///   .with_cancel_token(token)
/// // 另一处(另一个协程、或某个 UI 回调里):
/// token.cancel(message="用户点了停止")
/// ```
pub fn Config::with_cancel_token(
  self : Config,
  cancel_token : CancelToken,
) -> Config {
  { ..self, cancel_token: Some(cancel_token), }
}