///|
/// Value lineage.
///
/// The gateway does not trust labels the agent attaches to data, and it does
/// not try to understand natural language. It does something narrower and
/// verifiable: when a tool result is declared sensitive by policy, every
/// secret-shaped value in that result is fingerprinted, and any later outbound
/// call whose arguments contain the same value is refused.
///
/// This is exact-value taint, not semantic classification. It catches the
/// common and damaging case — a credential read in one step and shipped out in
/// the next — and it deliberately fails closed: a value that looks like a
/// secret and appears in both places blocks the call even if the agent's
/// intent was benign.
pub struct TaintSource {
  call : String
  tool : String
  preview : String
}

///|
pub fn taint_source(
  call : String,
  tool : String,
  preview : String,
) -> TaintSource {
  { call, tool, preview, }
}

///|
/// A token that survived the shape filter. `preview` is safe to log: four
/// leading characters are enough for a human to recognise which credential is
/// involved, and not enough to reuse it.
pub struct SecretToken {
  fingerprint : String
  preview : String
}

///|
fn is_token_char(ch : Char) -> Bool {
  (ch >= 'a' && ch <= 'z') ||
  (ch >= 'A' && ch <= 'Z') ||
  (ch >= '0' && ch <= '9') ||
  ch == '_' ||
  ch == '-' ||
  ch == '.'
}

///|
fn has_digit(text : String) -> Bool {
  for ch in text {
    if ch >= '0' && ch <= '9' {
      return true
    }
  }
  false
}

///|
fn has_upper(text : String) -> Bool {
  for ch in text {
    if ch >= 'A' && ch <= 'Z' {
      return true
    }
  }
  false
}

///|
fn skip_spaces(chars : Array[Char], from : Int32) -> Int32 {
  let mut index = from
  while index < chars.length() && (chars[index] == ' ' || chars[index] == '\t') {
    index += 1
  }
  index
}

///|
/// Field names are not secrets. A token directly followed by `=` is an
/// environment-style name (`OPENAI_API_KEY`), and a quoted token followed by
/// `:` is a JSON object key. Skipping both keeps the taint set focused on
/// values, which is what actually leaves the machine.
fn looks_like_field_name(
  chars : Array[Char],
  start : Int32,
  end : Int32,
) -> Bool {
  let after = skip_spaces(chars, end)
  if after < chars.length() && chars[after] == '=' {
    return true
  }
  if start > 0 &&
    chars[start - 1] == '"' &&
    after < chars.length() &&
    chars[after] == '"' {
    let colon = skip_spaces(chars, after + 1)
    if colon < chars.length() && chars[colon] == ':' {
      return true
    }
  }
  false
}

///|
/// A raw secret value plus its identifiers. The raw value is only ever kept in
/// the session vault; ledger entries and errors carry the fingerprint and a
/// four-character preview.
pub struct RawToken {
  value : String
  fingerprint : String
  preview : String
}

///|
/// Extract raw secret-shaped values for masking. Callers must not log `value`.
pub fn extract_raw(text : String, min_len : Int32) -> Array[RawToken] {
  let out : Array[RawToken] = []
  let chars = text.to_array()
  let length = chars.length()
  let mut i = 0
  while i < length {
    if is_token_char(chars[i]) {
      let start = i
      while i < length && is_token_char(chars[i]) {
        i += 1
      }
      let end = i
      if end - start >= min_len && !looks_like_field_name(chars, start, end) {
        let token = text[start:end].to_owned()
        if token.length() <= 512 && (has_digit(token) || has_upper(token)) {
          out.push({
            value: token,
            fingerprint: fingerprint_of(token),
            preview: token[0:4].to_owned() + "…",
          })
        }
      }
    } else {
      i += 1
    }
  }
  out
}

///|
/// Extract secret-shaped values from one string.
///
/// The scanner accepts runs of `[A-Za-z0-9_.-]` at least `min_len` long that
/// contain a digit or an uppercase letter. Requiring a digit or uppercase
/// keeps ordinary prose ("application", "workspace") out of the taint set
/// while still admitting API keys, tokens, connection strings and long
/// identifiers. Names on the left of `=` or `:` are excluded on purpose: they
/// are the most common false positive, and a blocked call that only contains a
/// variable name erodes trust in the gateway.
pub fn extract_tokens(text : String, min_len : Int32) -> Array[SecretToken] {
  let unique : Array[SecretToken] = []
  let seen : Map[String, Bool] = Map([])
  for raw in extract_raw(text, min_len) {
    if !seen.contains(raw.fingerprint) {
      seen.set(raw.fingerprint, true)
      unique.push({ fingerprint: raw.fingerprint, preview: raw.preview, })
    }
  }
  unique
}

///|
/// Collect every string leaf of a JSON value. Used on `tools/call` arguments
/// so taint matching sees values, not JSON syntax (keys, punctuation, escapes).
pub fn collect_strings(value : Json) -> Array[String] {
  let out : Array[String] = []
  collect_strings_into(value, out)
  out
}

///|
fn collect_strings_into(value : Json, out : Array[String]) -> Unit {
  match value {
    String(s) => out.push(s)
    Array(items) =>
      for item in items {
        collect_strings_into(item, out)
      }
    Object(fields) =>
      for _, item in fields {
        collect_strings_into(item, out)
      }
    _ => ()
  }
}

///|
/// Fingerprints of every secret-shaped value in the given strings.
pub fn scan_fingerprints(
  texts : Array[String],
  min_len : Int32,
) -> Array[SecretToken] {
  let unique : Array[SecretToken] = []
  let seen : Map[String, Bool] = Map([])
  for text in texts {
    for token in extract_tokens(text, min_len) {
      if !seen.contains(token.fingerprint) {
        seen.set(token.fingerprint, true)
        unique.push(token)
      }
    }
  }
  unique
}

///|
/// One taint hit: the fingerprinted value, plus the tool call that introduced
/// it. This is the evidence the gateway returns to the agent and writes to the
/// ledger.
pub struct TaintHit {
  fingerprint : String
  preview : String
  source : TaintSource
}

///|
pub fn find_taint(
  taint : Map[String, TaintSource],
  texts : Array[String],
  min_len : Int32,
) -> Array[TaintHit] {
  let hits : Array[TaintHit] = []
  let seen : Map[String, Bool] = Map([])
  for text in texts {
    // As written.
    for token in scan_fingerprints([text], min_len) {
      match taint.get(token.fingerprint) {
        Some(source) =>
          push_hit(hits, seen, token.fingerprint, token.preview, source)
        None => ()
      }
    }
    // Separators removed and case folded: catches `sk-abc_123` sent as
    // `skabc123` or `SK ABC 123`.
    let collapsed = collapse_separators(text)
    if collapsed != text {
      for token in scan_fingerprints([collapsed], min_len) {
        match taint.get(token.fingerprint) {
          Some(source) =>
            push_hit(hits, seen, token.fingerprint, token.preview, source)
          None => ()
        }
      }
    }
    // Base64-shaped runs: catches a secret that was encoded before leaving.
    for run in scan_base64_like(text) {
      let fingerprint = fingerprint_of(run)
      match taint.get(fingerprint) {
        Some(source) =>
          push_hit(hits, seen, fingerprint, run[0:4].to_owned() + "…", source)
        None => ()
      }
    }
  }
  hits
}

///|
/// Placeholder syntax used in tool results and arguments: `«mg:»`.
/// The delimiters are non-ASCII on purpose so that model-generated prose is
/// unlikely to collide with ordinary content.
pub fn placeholder_for(fingerprint : String) -> String {
  "«mg:" + fingerprint + "»"
}

///|
fn is_hex_lower(ch : Char) -> Bool {
  (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f')
}

///|
/// Fingerprints referenced by placeholders in one string.
pub fn find_placeholders(text : String) -> Array[String] {
  let out : Array[String] = []
  let chars = text.to_array()
  let length = chars.length()
  let mut i = 0
  while i < length {
    if chars[i] == '«' && i + 4 <= length {
      let matched = chars[i + 1] == 'm' &&
        chars[i + 2] == 'g' &&
        chars[i + 3] == ':'
      if matched {
        let start = i + 4
        let mut end = start
        while end < length && is_hex_lower(chars[end]) {
          end += 1
        }
        if end - start == 16 && end < length && chars[end] == '»' {
          out.push(text[start:end].to_owned())
          i = end + 1
          continue
        }
      }
    }
    i += 1
  }
  out
}

///|
/// Replace placeholders with their real values. Returns the rewritten string
/// and how many distinct placeholders were substituted. Unknown placeholders
/// are left alone.
pub fn reveal_placeholders(
  text : String,
  vault : Map[String, String],
) -> (String, Int32) {
  let mut result = text
  let mut count = 0
  let seen : Map[String, Bool] = Map([])
  for fingerprint in find_placeholders(text) {
    if seen.contains(fingerprint) {
      continue
    }
    seen.set(fingerprint, true)
    match vault.get(fingerprint) {
      Some(value) => {
        result = result.replace_all(old=placeholder_for(fingerprint), new=value)
        count += 1
      }
      None => ()
    }
  }
  (result, count)
}

///|
/// Rewrite a JSON value by passing every string leaf through `transform`.
pub fn map_json_strings(value : Json, transform : (String) -> String) -> Json {
  match value {
    String(s) => Json::string(transform(s))
    Array(items) => {
      let out : Array[Json] = []
      for item in items {
        out.push(map_json_strings(item, transform))
      }
      Json::array(out)
    }
    Object(fields) => {
      let out : Map[String, Json] = Map([])
      for key, item in fields {
        out.set(key, map_json_strings(item, transform))
      }
      Json::object(out)
    }
    _ => value
  }
}

///|
/// Raw tokens across several strings, deduplicated by fingerprint.
pub fn scan_raw(texts : Array[String], min_len : Int32) -> Array[RawToken] {
  let unique : Array[RawToken] = []
  let seen : Map[String, Bool] = Map([])
  for text in texts {
    for raw in extract_raw(text, min_len) {
      if !seen.contains(raw.fingerprint) {
        seen.set(raw.fingerprint, true)
        unique.push(raw)
      }
    }
  }
  unique
}

///|
/// Distinct placeholder fingerprints anywhere in a JSON value.
pub fn placeholders_in(value : Json) -> Array[String] {
  let unique : Array[String] = []
  let seen : Map[String, Bool] = Map([])
  for text in collect_strings(value) {
    for fingerprint in find_placeholders(text) {
      if !seen.contains(fingerprint) {
        seen.set(fingerprint, true)
        unique.push(fingerprint)
      }
    }
  }
  unique
}

///|
fn to_lower_ascii(ch : Char) -> Char {
  if ch >= 'A' && ch <= 'Z' {
    (ch.to_int() + 32).unsafe_to_char()
  } else {
    ch
  }
}

///|
/// Drop separators and fold case, so `sk-abc_123`, `SK ABC 123` and
/// `skabc123` all collapse to the same string. A secret that is split or
/// re-cased before leaving still matches the collapsed view.
pub fn collapse_separators(text : String) -> String {
  let out = StringBuilder()
  for ch in text {
    if ch == '-' ||
      ch == '_' ||
      ch == '.' ||
      ch == ':' ||
      ch == '/' ||
      ch == ' ' ||
      ch == '\t' ||
      ch == '\n' {
      continue
    }
    out.write_string(to_lower_ascii(ch).to_string())
  }
  out.to_string()
}

///|
fn base64_char(value : Int32) -> Char {
  if value < 26 {
    (65 + value).unsafe_to_char()
  } else if value < 52 {
    (97 + value - 26).unsafe_to_char()
  } else if value < 62 {
    (48 + value - 52).unsafe_to_char()
  } else if value == 62 {
    '+'
  } else {
    '/'
  }
}

///|
/// Standard Base64 for the ASCII token shapes the scanner accepts.
pub fn base64_encode(text : String) -> String {
  let chars = text.to_array()
  let len = chars.length()
  let out = StringBuilder()
  let mut i = 0
  while i + 2 < len {
    let a = chars[i].to_int() & 255
    let b = chars[i + 1].to_int() & 255
    let c = chars[i + 2].to_int() & 255
    out.write_string(base64_char(a >> 2).to_string())
    out.write_string(base64_char(((a & 3) << 4) | (b >> 4)).to_string())
    out.write_string(base64_char(((b & 15) << 2) | (c >> 6)).to_string())
    out.write_string(base64_char(c & 63).to_string())
    i += 3
  }
  let rest = len - i
  if rest == 1 {
    let a = chars[i].to_int() & 255
    out.write_string(base64_char(a >> 2).to_string())
    out.write_string(base64_char((a & 3) << 4).to_string())
    out.write_string("==")
  } else if rest == 2 {
    let a = chars[i].to_int() & 255
    let b = chars[i + 1].to_int() & 255
    out.write_string(base64_char(a >> 2).to_string())
    out.write_string(base64_char(((a & 3) << 4) | (b >> 4)).to_string())
    out.write_string(base64_char((b & 15) << 2).to_string())
    out.write_string("=")
  }
  out.to_string()
}

///|
pub fn base64_urlsafe(text : String) -> String {
  base64_encode(text)
  .replace_all(old="+", new="-")
  .replace_all(old="/", new="_")
}

///|
/// Encoded forms a secret can take on the way out. The gateway stores the
/// fingerprint of each form when the secret is first seen, so a later call
/// that carries any of them is still recognised as the same value.
pub fn variant_values(value : String) -> Array[String] {
  let out : Array[String] = []
  let collapsed = collapse_separators(value)
  if collapsed != value && collapsed.length() >= 6 {
    out.push(collapsed)
  }
  if value.length() >= 8 {
    out.push(base64_encode(value))
    out.push(base64_urlsafe(value))
  }
  out
}

///|
fn is_base64_char(ch : Char) -> Bool {
  (ch >= 'A' && ch <= 'Z') ||
  (ch >= 'a' && ch <= 'z') ||
  (ch >= '0' && ch <= '9') ||
  ch == '+' ||
  ch == '/' ||
  ch == '='
}

///|
/// Runs that look like Base64 and are long enough to be an encoded secret.
/// Requiring alignment and a digit keeps ordinary words out.
fn scan_base64_like(text : String) -> Array[String] {
  let out : Array[String] = []
  let chars = text.to_array()
  let len = chars.length()
  let mut i = 0
  while i < len {
    if is_base64_char(chars[i]) {
      let start = i
      let mut digit = false
      while i < len && is_base64_char(chars[i]) {
        let ch = chars[i]
        if ch >= '0' && ch <= '9' {
          digit = true
        }
        i += 1
      }
      let run = text[start:i].to_owned()
      if digit && run.length() >= 12 && run.length() % 4 == 0 {
        out.push(run)
      }
    } else {
      i += 1
    }
  }
  out
}

///|
fn push_hit(
  hits : Array[TaintHit],
  seen : Map[String, Bool],
  fingerprint : String,
  preview : String,
  source : TaintSource,
) -> Unit {
  // The same secret may be recognised through several views (as written,
  // collapsed, Base64). They all point at one source, so report it once.
  let key = source.call + "|" + source.tool + "|" + source.preview
  if !seen.contains(key) {
    seen.set(key, true)
    hits.push({ fingerprint, preview, source, })
  }
}