// tally —— 门面 API 的最短路径。
//
// 四层结构(见计划 §0):`bpe`(内核)-> `vocab`(词表)-> `budget`(预算)-> `tally`(门面)。
// 本包只做转发与再导出,不含任何算法与 IO。

///|
/// 再导出核心类型,让使用者只 import 一个包。
pub using @vocab {
  type Vocab,
  type VocabError,
  type Merge,
  type TrainResult,
}

///|
pub using @budget {
  type Priority,
  type Segment,
  type KeptSegment,
  type TruncatedSegment,
  type DroppedSegment,
  type Plan,
  type Counter,
  type FitConfig,
  type TruncateStrategy,
  type PoolQuota,
  type PoolUsage,
  type FitError,
  type DigestError,
  type PlanDigest,
  type Seal,
  type SealError,
}

///|
/// 一段不可裁的上下文(system 提示、工具定义)。
///
/// 门面包提供这四个构造器,是为了让「只 import `weopqrst/tally`」也能装段:
/// 枚举构造器 `Pinned` / `Newest` / `Middle` / `Oldest` 无法经 `pub using` 再导出,
/// 而 `fit` 是主路径,不该逼使用者再依赖 `weopqrst/tally/budget`。
/// 截断策略同理,见 `truncate_none` / `truncate_tail` / `truncate_head`。
pub fn pinned(role : String, text : String) -> @budget.Segment {
  @budget.Segment::text(role, @budget.Pinned, text)
}

///|
/// 最近若干轮:对下一步生成影响最大,优先保留。
pub fn newest(role : String, text : String) -> @budget.Segment {
  @budget.Segment::text(role, @budget.Newest, text)
}

///|
/// 中间历史:第一批牺牲品之一。
pub fn middle(role : String, text : String) -> @budget.Segment {
  @budget.Segment::text(role, @budget.Middle, text)
}

///|
/// 最早的历史:最先被丢,权重相同时下标更小者先丢。
pub fn oldest(role : String, text : String) -> @budget.Segment {
  @budget.Segment::text(role, @budget.Oldest, text)
}

///|
/// 编码任意字节序列为 token id。
pub fn encode(v : @vocab.Vocab, bytes : Bytes) -> Array[Int] {
  @vocab.Vocab::encode_bytes(v, bytes)
}

///|
/// 编码一段文本。
pub fn encode_string(v : @vocab.Vocab, text : String) -> Array[Int] {
  @vocab.Vocab::encode_string(v, text)
}

///|
/// token id 序列 -> 原始字节。
pub fn decode(v : @vocab.Vocab, tokens : Array[Int]) -> Bytes {
  @vocab.Vocab::decode_ids(v, tokens)
}

///|
/// 字节序列的 token 数。
pub fn count_bytes(v : @vocab.Vocab, bytes : Bytes) -> Int64 {
  @vocab.Vocab::count_bytes(v, bytes)
}

///|
/// 文本的 token 数。
pub fn count(v : @vocab.Vocab, text : String) -> Int64 {
  @vocab.Vocab::count(v, text)
}

///|
/// 逐片明细:`(片文本, 该片的 token 数)`。
pub fn piece_counts(v : @vocab.Vocab, bytes : Bytes) -> Array[(String, Int64)] {
  @vocab.Vocab::piece_counts(v, bytes)
}

///|
/// 精确计数器:给定词表时逐 token 计数。
///
/// 适配器住在门面包:`budget` 只认 `Counter`,包级不依赖词表——所以
/// `import weopqrst/tally/budget` 不会把整份 vocab 拖进来。
pub fn cl100k(v : @vocab.Vocab) -> @budget.Counter {
  {
    name: "cl100k_base",
    exact: true,
    count_bytes: fn(bytes) { @vocab.Vocab::count_bytes(v, bytes) },
  }
}

///|
/// 估算计数器:`chars_per_token` 个字符当 1 token。报告里会标 `exact=false`。
pub fn estimate(chars_per_token : Int) -> @budget.Counter {
  @budget.Counter::estimate(chars_per_token)
}

///|
/// 装配配置:整窗可用、整段丢弃。用 `.with_reserve(..)` / `.with_strategy(..)` 调整。
pub fn fit_config(counter : @budget.Counter, window : Int64) -> @budget.FitConfig {
  @budget.FitConfig::new(counter, window)
}

///|
/// 给配置加池配额:`(池名, 配额)`,按数组顺序处理借用。
///
/// 语义:池内段共享这份额度;某池用不完的份额按声明顺序借给超额的池;
/// 借完仍超额就在该池内部继续按优先级/分数裁剪。台账写在报告的 `[pools]` 节。
pub fn fit_config_pools(
  cfg : @budget.FitConfig,
  pools : Array[(String, Int64)],
) -> @budget.FitConfig {
  let quotas : Array[@budget.PoolQuota] = []
  for p in pools {
    quotas.push(@budget.PoolQuota::new(p.0, p.1))
  }
  cfg.with_pools(quotas)
}

///|
/// 把一段归入配额池(段名不得含空格)。
pub fn with_pool(seg : @budget.Segment, name : String) -> @budget.Segment {
  seg.with_pool(name)
}

///|
/// 设置同优先级内的取舍分数:越大越优先保留。
pub fn with_score(seg : @budget.Segment, score : Int64) -> @budget.Segment {
  seg.with_score(score)
}

///|
/// 整段丢弃(默认)。
pub fn truncate_none() -> @budget.TruncateStrategy {
  @budget.NoTruncate
}

///|
/// 保留段首、截掉段尾(`system` 提示这类开头最重要的内容)。
pub fn truncate_tail() -> @budget.TruncateStrategy {
  @budget.KeepPrefix
}

///|
/// 保留段尾、截掉段首(历史/日志这类最近内容最重要的段)。
pub fn truncate_head() -> @budget.TruncateStrategy {
  @budget.KeepSuffix
}

///|
/// 保留段首,把被截掉的尾部换成占位标记并标注省略量(长日志/长工具输出用)。
///
/// 零依赖下的「占位压缩」就是这一件事:标记文案固定、只带省略量,不做语义摘要;
/// 标记本身占的 token 计入窗口,报告里 `omitted=` 与标记里的数字一致。
pub fn truncate_compress() -> @budget.TruncateStrategy {
  @budget.Compress
}

///|
/// 装配上下文:按优先级丢段/截短,并给出可解释的报告。
pub fn fit_with(
  cfg : @budget.FitConfig,
  segments : Array[@budget.Segment],
) -> @budget.Plan raise @budget.FitError {
  @budget.fit_with(cfg, segments)
}

///|
/// 同 `fit_with`,但不 `raise`:配置不成立时返回 `Err((reserve, window))`。
///
/// 为什么需要它:`pub using` 能再导出 `FitError` **类型**,却导不出 suberror
/// 构造器,只 import 门面包的使用者无法把 `ReserveExceedsWindow` 写进 `catch`。
/// 「显式报错」得有一条不依赖错误类型名字的通道。
pub fn fit_with_result(
  cfg : @budget.FitConfig,
  segments : Array[@budget.Segment],
) -> Result[@budget.Plan, (Int64, Int64)] {
  try {
    Ok(@budget.fit_with(cfg, segments))
  } catch {
    @budget.ReserveExceedsWindow(reserve, window) => Err((reserve, window))
  }
}

///|
/// 计划的摘要侧车(`tally-digest v1` 文本),与 `plan.render()` 配对。
///
/// 报告给人读,摘要给机器验:报告字节一个 sha256、每段进了窗口的字节各一个。
pub fn render_digest(plan : @budget.Plan) -> String {
  @budget.Plan::render_digest(plan)
}

///|
/// 校验报告与摘要是否自洽。
///
/// `Ok(())` 或者一句带位置的 `Err`(哪个文件、第几行、两个 sha256 各是什么)。
/// 这里返回 `Result` 而不是 `raise`,理由同 `fit_with_result`。
pub fn verify_report(
  report : String,
  digest_text : String,
) -> Result[Unit, String] {
  @budget.verify_report(report, digest_text)
}

///|
/// 给摘要盖印记(`tally-seal v1`,HMAC-SHA256)。
///
/// 印记回答的是"有没有人认过这份摘要",不是"谁写的"——对称密钥证明持有者,
/// 不证明身份。返回 `Result` 而不是 `raise`,理由同 `verify_report`。
pub fn seal_digest(
  digest_text : String,
  key : String,
  key_id : String,
) -> Result[String, String] {
  @budget.seal_digest_result(digest_text, key, key_id)
}

///|
/// 校验「摘要 + 印记」:摘要没被换、印记确由这把密钥盖出。
pub fn verify_seal(
  digest_text : String,
  seal_text : String,
  key : String,
) -> Result[Unit, String] {
  @budget.verify_seal(digest_text, seal_text, key)
}

///|
/// 装配上下文:按优先级整段丢弃(0.3 的兼容入口)。
///
/// 与 `fit_with(fit_config(cl100k(v), limit), segments)` 输出逐字节相同;
/// 不校验 `limit > 0`(旧语义:预算为 0 时全部丢弃、Pinned 溢出上报)。
pub fn fit(
  v : @vocab.Vocab,
  segments : Array[@budget.Segment],
  limit : Int64,
) -> @budget.Plan {
  @budget.assemble(@budget.FitConfig::new(cl100k(v), limit), segments)
}

///|
/// 由三块数据构造一个经过校验的词表(`specials` 为 `(字面量, id)`)。
pub fn build_vocab(
  tokens : Array[Bytes],
  ranks : Map[Bytes, Int],
  specials : Array[(String, Int)],
) -> @vocab.Vocab raise @vocab.VocabError {
  @vocab.build_vocab(tokens, ranks, specials)
}

///|
/// 加载自有规范文本格式的词表。
pub fn load(text : String) -> @vocab.Vocab raise @vocab.VocabError {
  @vocab.load(text)
}

///|
/// 加载 `.tiktoken` 风格词表。
pub fn load_tiktoken(
  text : String,
  specials : Array[(String, Int)],
) -> @vocab.Vocab raise @vocab.VocabError {
  @vocab.load_tiktoken(text, specials)
}

///|
/// 导出为自有规范文本格式。
pub fn to_text(v : @vocab.Vocab) -> String {
  @vocab.Vocab::to_text(v)
}

///|
/// 生成表所用的 Unicode 版本(用于报告与 README 的可复算声明)。
pub fn unicode_version() -> String {
  @bpe.unicode_version()
}

///|
/// 用 byte-level BPE 训练一份词表:给定语料与参数,两次训练**逐字节一致**。
///
/// tie-break 规则(全序,无并列):pair 频次降序 -> `left` 字节序升序 ->
/// `right` 字节序升序。规则与实现在 `vocab/train.mbt`。
pub fn train(
  corpus : Array[Bytes],
  vocab_size : Int,
  min_frequency : Int,
  specials : Array[String],
) -> @vocab.TrainResult raise @vocab.VocabError {
  @vocab.train(corpus, vocab_size, min_frequency, specials)
}

///|
/// 回读 `tally-train v1` 训练产物(词表一并重建)。
pub fn load_train(text : String) -> @vocab.TrainResult raise @vocab.VocabError {
  @vocab.load_train(text)
}