// mooncassette/sanitize —— 录制内容的脱敏。
//
// 为什么这是**默认行为**而不是可选功能:
// cassette 会被提交进版本控制、被贴进 issue、被用于演示。
// 一个不小心录进去的 API Key,等同于把密钥永久写进了 git 历史。
//
// 关于指纹的语义:脱敏会改变请求内容,从而改变指纹。
// 本项目刻意采用「指纹在编码时由存储内容派生」而不是「提前存死」,
// 因此「先脱敏、再落盘」得到的指纹始终自洽,完整性校验不会误报。

///|
/// 脱敏策略。
pub(all) struct SanitizePolicy {
  /// 大小写不敏感的**精确**键名匹配。
  exact_keys : Array[String]
  /// 大小写不敏感的**后缀**匹配,例如 `_token` 命中 `access_token`。
  ///
  /// 之所以用后缀而不是子串,是为了避免误伤:`max_tokens`、`total_tokens`
  /// 以 `_tokens` 结尾,不会被 `_token` 命中,因而不会被误替换。
  key_suffixes : Array[String]
  /// 命中后的替换值。
  placeholder : String
  /// 是否额外扫描字符串值中「形如密钥」的子串(例如 `sk-...`)。
  scan_secret_shapes : Bool
} derive(Eq)

///|
/// 默认策略:覆盖常见厂商的鉴权字段,并开启密钥形状扫描。
pub fn SanitizePolicy::default() -> SanitizePolicy {
  {
    exact_keys: [
      "authorization", "proxy-authorization", "api-key", "x-api-key", "api_key",
      "apikey", "token", "cookie", "set-cookie", "password",
    ],
    key_suffixes: ["_key", "_token", "_secret", "_password", "_credential"],
    placeholder: "***REDACTED***",
    scan_secret_shapes: true,
  }
}

///|
/// 判断某个键名是否需要脱敏。
pub fn is_sensitive_key(key : String, policy : SanitizePolicy) -> Bool {
  let lowered = ascii_lowercase(key)
  for i = 0; i < policy.exact_keys.length(); i = i + 1 {
    if lowered == ascii_lowercase(policy.exact_keys[i]) {
      return true
    }
  }
  for i = 0; i < policy.key_suffixes.length(); i = i + 1 {
    let suffix = ascii_lowercase(policy.key_suffixes[i])
    if lowered.length() >= suffix.length() && lowered.has_suffix(suffix) {
      return true
    }
  }
  false
}

///|
/// 递归脱敏一个 JSON 值。
///
/// 键名命中策略时,**整个值**被替换为占位符(不尝试保留部分内容);
/// 字符串值若开启 `scan_secret_shapes`,则额外做子串级替换。
pub fn sanitize_json(value : Json, policy : SanitizePolicy) -> Json {
  match value {
    Object(fields) => {
      let out : Map[String, Json] = Map([])
      for key in fields.keys() {
        match fields.get(key) {
          Some(child) =>
            if is_sensitive_key(key, policy) {
              out.set(key, Json::string(policy.placeholder))
            } else {
              out.set(key, sanitize_json(child, policy))
            }
          None => ()
        }
      }
      Json::object(out)
    }
    Array(items) => {
      let out : Array[Json] = []
      for item in items {
        out.push(sanitize_json(item, policy))
      }
      Json::array(out)
    }
    String(text) =>
      if policy.scan_secret_shapes {
        Json::string(redact_secret_shapes(text, policy.placeholder))
      } else {
        value
      }
    scalar => scalar
  }
}

///|
/// 脱敏一个请求。
pub fn sanitize_request(
  request : @core.Request,
  policy : SanitizePolicy,
) -> @core.Request {
  {
    provider: request.provider,
    model: request.model,
    body: sanitize_json(request.body, policy),
  }
}

///|
/// 脱敏一个响应。
///
/// `status` 与 `usage` 原样保留:它们是数值元数据,不承载密钥。
pub fn sanitize_response(
  response : @core.Response,
  policy : SanitizePolicy,
) -> @core.Response {
  {
    status: response.status,
    body: sanitize_json(response.body, policy),
    usage: response.usage,
  }
}

///|
/// 脱敏一条交互记录。
pub fn sanitize_interaction(
  interaction : @core.Interaction,
  policy : SanitizePolicy,
) -> @core.Interaction {
  {
    request: sanitize_request(interaction.request, policy),
    response: sanitize_response(interaction.response, policy),
  }
}

///|
/// 脱敏整个 cassette,返回新的 cassette(不修改入参)。
///
/// `meta` 原样保留:它由调用方掌控,不来自模型或网络。
pub fn sanitize_cassette(
  cassette : @core.Cassette,
  policy : SanitizePolicy,
) -> @core.Cassette {
  let interactions : Array[@core.Interaction] = []
  for item in cassette.interactions {
    interactions.push(sanitize_interaction(item, policy))
  }
  { version: cassette.version, meta: cassette.meta, interactions }
}

///|
/// 扫描字符串中「形如密钥」的子串并替换。
///
/// 刻意保守:只识别 `sk-` 这一类高置信度前缀,且要求后续至少 8 个
/// 密钥字符。宁可漏掉一个,也不要误判 —— 误判会把正常内容替换掉,
/// 让 cassette 失去作为测试基准的价值。
fn redact_secret_shapes(text : String, placeholder : String) -> String {
  let chars : Array[Char] = []
  for ch in text {
    chars.push(ch)
  }
  let total = chars.length()
  let out = StringBuilder::new()
  let mut index = 0
  while index < total {
    match secret_shape_end(chars, index) {
      Some(end) => {
        out.write_string(placeholder)
        index = end
      }
      None => {
        out.write_char(chars[index])
        index = index + 1
      }
    }
  }
  out.to_string()
}

///|
/// 若 `chars` 从 `start` 起是一个密钥形状子串,返回其结束下标(不含)。
/// 否则返回 `None`。
fn secret_shape_end(chars : Array[Char], start : Int) -> Int? {
  let total = chars.length()
  if start + 3 > total {
    return None
  }
  if !(chars[start] == 's' && chars[start + 1] == 'k' && chars[start + 2] == '-') {
    return None
  }
  let mut end = start + 3
  while end < total && is_secret_body_char(chars[end]) {
    end = end + 1
  }
  if end - (start + 3) >= 8 {
    Some(end)
  } else {
    None
  }
}

///|
/// 密钥主体允许出现的字符。
fn is_secret_body_char(ch : Char) -> Bool {
  ch.is_ascii_alphabetic() || ch.is_ascii_digit() || ch == '-' || ch == '_'
}

///|
/// ASCII 小写化。
///
/// 只处理 ASCII:键名比较场景足够,且避免引入 Unicode 大小写折叠的
/// 版本依赖(不同 Unicode 版本折叠结果可能不同,会破坏指纹稳定性)。
fn ascii_lowercase(text : String) -> String {
  let sb = StringBuilder::new()
  for ch in text {
    sb.write_char(ch.to_ascii_lowercase())
  }
  sb.to_string()
}