#!/usr/bin/env bash
#
# Claude Code statusline for the Weave Router. CC pipes a JSON blob on stdin
# whose `transcript_path` points at the current session's JSONL log. Model
# names and price comparisons come from that local transcript; neither
# establishes the request's destination, including when the transcript
# contains an old router pin acknowledgement.
#
# Wire up by adding to ~/.claude/settings.json:
#   { "statusLine": { "type": "command", "command": "/abs/path/to/cc-statusline.sh" } }
#
# Renders:
#   WEAVE ROUTER · response model: claude-sonnet-4-5 · selected: claude-opus-4-7 · est. cost difference $1.23
#
# Pricing source of truth: internal/router/catalog. Input/output prices and
# cache-read multipliers are generated by cmd/genprices. Cache creation remains
# at 1.25× input pending TTL-aware pricing.

set -euo pipefail
export PATH="$PATH:$HOME/.weave/bin"

# ---------- background self-refresh ----------
#
# Once every WEAVE_STATUSLINE_UPDATE_INTERVAL_DAYS (default 7), check
# raw.githubusercontent.com for a newer copy of this script and swap it in
# atomically. Runs in a forked subshell so the current Claude turn never
# blocks; the next turn picks up the new version. Applies to both user-scope
# (~/.weave/cc-statusline.sh) and project-scope (<repo>/.claude/cc-statusline.sh)
# installs — project teammates rate-limit independently because the stamp
# lives in their per-user cache dir, and on no-content-change days we skip
# the mv entirely so the repo working tree stays clean. When upstream does
# change, the first teammate's commit propagates the new version to the rest.
# A pricing-table miss also triggers a refresh off-schedule — see
# weave_refresh_on_price_miss below.
#
# Opt out entirely with `export WEAVE_STATUSLINE_UPDATE=0`. Override the
# source with `WEAVE_STATUSLINE_URL=...`, e.g. for self-hosters who fork.
#
# $1 is an optional stamp suffix, so a caller refreshing for a reason other than
# "the interval elapsed" rate-limits on its own clock rather than sharing the
# periodic check's budget. $2=1 drops the stamp again if the download fails, for
# callers recovering from a known-bad state where waiting out the full interval
# on a transient network error is worse than retrying next turn.
weave_self_refresh() {
  local stamp_suffix="${1:-}"
  local retry_on_fail="${2:-0}"

  [ "${WEAVE_STATUSLINE_UPDATE:-1}" = "0" ] && return 0
  command -v curl >/dev/null 2>&1 || return 0

  local self="${BASH_SOURCE[0]:-$0}"
  [ -f "$self" ] && [ -w "$self" ] || return 0

  local interval_days="${WEAVE_STATUSLINE_UPDATE_INTERVAL_DAYS:-7}"
  local interval_seconds=$(( interval_days * 86400 ))

  # Stamp lives in the per-user cache dir, keyed by absolute script path so
  # multiple repos (and the user-scope copy) rate-limit independently and no
  # stray file ever lands inside a repo working tree.
  local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/weave-router"
  mkdir -p "$cache_dir" 2>/dev/null || return 0
  local script_slug
  script_slug="$(printf '%s' "$self" | tr -c 'A-Za-z0-9._-' '_')"
  local stamp="$cache_dir/checked-at${script_slug}${stamp_suffix}"

  local now stamp_mtime
  now="$(date +%s 2>/dev/null)" || return 0
  if [ -f "$stamp" ]; then
    # Try GNU `stat -c %Y` first; on macOS (BSD stat) -c isn't recognized
    # and exits non-zero, so we fall through to `stat -f %m`. The reverse
    # order is broken: GNU `stat -f` is `--file-system`, which silently
    # succeeds with multi-line filesystem info instead of failing, leaving
    # $stamp_mtime as garbage and disabling the rate-limit check entirely.
    stamp_mtime="$(stat -c %Y "$stamp" 2>/dev/null || stat -f %m "$stamp" 2>/dev/null)" || stamp_mtime=0
  else
    stamp_mtime=0
  fi
  if [ -n "${stamp_mtime:-}" ] && [ "$stamp_mtime" -gt 0 ] \
     && [ $(( now - stamp_mtime )) -lt "$interval_seconds" ]; then
    return 0
  fi

  # Touch the stamp BEFORE forking so concurrent statusline invocations
  # (Claude calls us on every turn) don't all kick off downloads.
  : > "$stamp" 2>/dev/null || return 0

  local url="${WEAVE_STATUSLINE_URL:-https://raw.githubusercontent.com/weave-os/router/main/install/cc-statusline.sh}"
  # $$ alone is not unique: two calls can run in one invocation (the periodic
  # check and a pricing-miss retry both fire on a cold cache) and would then
  # curl -o into the same path and mv over each other, installing a truncated
  # script that can never self-heal. The stamp suffix is the right key — it is
  # what makes two callers mutually exclusive in the first place, so callers
  # that could overlap necessarily have different suffixes.
  local tmp="${self}.tmp.$$${stamp_suffix}"
  (
    # Detach stdin (CC pipes JSON to us) so curl can't accidentally consume
    # it, and silence all output so nothing leaks into the statusline.
    exec </dev/null
    if curl -fsSL --max-time 15 "$url" -o "$tmp" 2>/dev/null \
       && [ -s "$tmp" ] \
       && head -n 1 "$tmp" | grep -q '^#!.*bash' \
       && [ "$(wc -c < "$tmp")" -ge 1024 ]; then
      # No-op when the download matches what's already on disk — keeps git
      # status clean for project-scope teammates during a routine refresh.
      if cmp -s "$tmp" "$self"; then
        rm -f "$tmp"
      else
        chmod +x "$tmp" 2>/dev/null || true
        mv "$tmp" "$self" 2>/dev/null || rm -f "$tmp"
      fi
    else
      rm -f "$tmp"
      # A download that never landed shouldn't spend the caller's whole
      # interval; drop the stamp so the next turn can try again.
      if [ "$retry_on_fail" = "1" ]; then
        rm -f "$stamp"
      fi
    fi
  ) >/dev/null 2>&1 &
  disown 2>/dev/null || true
  return 0
}
weave_self_refresh 2>/dev/null || true

# ---------- background slash-command refresh ----------
#
# The /force-model, /router-* … wrappers under <install>/.claude/commands/ are
# written once at install time and then never touched again, so a user who
# doesn't re-run the installer keeps whatever set shipped that day. This script
# is the only thing Claude Code invokes on every turn, so it doubles as the
# refresh point for them: same rate limit, same detached fork, same
# content-diff no-op as the self-refresh above.
#
# Never clobber a wrapper the user edited. The only way to tell an edit from a
# stale copy is to remember what we last downloaded, so each canonical file is
# cached (unrendered) per install under the user cache dir and a wrapper is
# replaced only when its bytes still match that baseline. With no baseline yet
# we can't prove anything, so the first run only seeds the cache and a swap can
# happen from the next one on. install.sh seeds it too, so fresh installs skip
# that warm-up round.
#
# Never touch a wrapper git tracks either. A project-scope install writes the
# wrappers into the repo's own .claude/commands/, and unlike cc-statusline.sh
# the installer does not gitignore them — so an unattended weekly rewrite would
# surface as unexplained dirty files (and could ride along in someone's commit).
# Only the installer changes tracked files, and only when a human runs it.
#
# Opt out with WEAVE_COMMANDS_UPDATE=0 (WEAVE_STATUSLINE_UPDATE=0 disables this
# too, along with every other network path here). Override the source with
# WEAVE_COMMANDS_URL_BASE=..., e.g. for self-hosters who fork.

# weave_command_tracked_by_git returns 0 when $1 is a file git tracks. Used to
# leave repo-committed wrappers alone; no git (or no repo) means untracked.
weave_command_tracked_by_git() {
  command -v git >/dev/null 2>&1 || return 1
  git -C "$(dirname "$1")" ls-files --error-unmatch -- "$1" >/dev/null 2>&1
}

# weave_installed_command_names lists the wrappers this install may refresh.
# The statusline ships standalone (no registry.sh beside it), so the installed
# set — itself written from the registry — is the only source of truth here.
#
# Files written before ownership markers existed carry none, so matching on the
# marker alone would freeze every pre-marker install out of refreshes forever.
# List every wrapper instead and let the baseline comparison below decide: a
# file is replaced only when its bytes still match the last canonical copy, so
# a user-authored command is never touched whether or not it carries a marker.
weave_installed_command_names() {
  local dir="$1" file
  for file in "$dir"/*.md; do
    [ -f "$file" ] || continue
    printf '%s\n' "$(basename "$file" .md)"
  done
}

# weave_render_command prints $1 with the installer's {{SCOPE}} placeholder
# replaced by $2, matching how install_slash_commands writes the same file.
# Trailing newlines are stripped on both sides of every comparison below.
weave_render_command() {
  local body
  body="$(cat "$1" 2>/dev/null)" || return 1
  printf '%s' "${body//\{\{SCOPE\}\}/$2}"
}

weave_sync_commands() {
  [ "${WEAVE_STATUSLINE_UPDATE:-1}" = "0" ] && return 0
  [ "${WEAVE_COMMANDS_UPDATE:-1}" = "0" ] && return 0
  command -v curl >/dev/null 2>&1 || return 0

  local self="${BASH_SOURCE[0]:-$0}" self_dir cmd_dir
  self_dir="$(cd "$(dirname "$self")" 2>/dev/null && pwd -P)" || return 0
  # User scope installs this script at <base>/.weave/, project and --dir at
  # <base>/.claude/. Claude Code reads commands from <base>/.claude/commands in
  # both layouts.
  case "${self_dir##*/}" in
    .weave)  cmd_dir="${self_dir%/*}/.claude/commands" ;;
    .claude) cmd_dir="$self_dir/commands" ;;
    *)       return 0 ;;
  esac
  [ -d "$cmd_dir" ] && [ -w "$cmd_dir" ] || return 0

  local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/weave-router"
  local dir_slug
  dir_slug="$(printf '%s' "$cmd_dir" | tr -c 'A-Za-z0-9._-' '_')"
  # Baseline is keyed by install, not shared: two installs refresh on their own
  # clocks, and a shared baseline updated by one would make the other's still-
  # canonical wrappers look user-edited and freeze them forever.
  local baseline_dir="$cache_dir/commands${dir_slug}"
  local stamp="$cache_dir/checked-at${dir_slug}.commands"
  mkdir -p "$baseline_dir" 2>/dev/null || return 0

  local interval_days="${WEAVE_STATUSLINE_UPDATE_INTERVAL_DAYS:-7}"
  local now stamp_mtime
  now="$(date +%s 2>/dev/null)" || return 0
  if [ -f "$stamp" ]; then
    stamp_mtime="$(stat -c %Y "$stamp" 2>/dev/null || stat -f %m "$stamp" 2>/dev/null)" || stamp_mtime=0
  else
    stamp_mtime=0
  fi
  if [ -n "${stamp_mtime:-}" ] && [ "$stamp_mtime" -gt 0 ] \
     && [ $(( now - stamp_mtime )) -lt $(( interval_days * 86400 )) ]; then
    return 0
  fi
  # Stamp before forking, same as the self-refresh: Claude Code calls us on
  # every turn and concurrent invocations must not all start downloading.
  : > "$stamp" 2>/dev/null || return 0

  # router-off/on/status bake this install's scope selector into their npx
  # line (upstream carries {{SCOPE}} there). Recover it from the installed copy
  # so a project or --dir install keeps toggling its own config; when it can't
  # be recovered those three are skipped rather than rewritten to point at the
  # user-scope install.
  local scope_args="" scope_known="false" off="$cmd_dir/router-off.md"
  # Backticks are literal Markdown delimiters.
  # shellcheck disable=SC2016
  if [ -f "$off" ] && grep -Eq '^`npx @(workweave/router|weave-os/router) off --claude.*`$' "$off" 2>/dev/null; then
    scope_known="true"
    # Backticks are literal Markdown delimiters.
    # shellcheck disable=SC2016
    scope_args="$(sed -En 's#^`npx @(workweave/router|weave-os/router) off --claude(.*)`$#\2#p' "$off" | head -n 1)"
  fi

  local url_base="${WEAVE_COMMANDS_URL_BASE:-https://raw.githubusercontent.com/weave-os/router/main/install/commands}"
  local name installed raw prev tmp new_body prev_body installed_body
  (
    # Detach stdin (CC pipes JSON to us) so curl can't consume it, and silence
    # everything so no output leaks into the statusline.
    exec </dev/null
    while IFS= read -r name; do
      installed="$cmd_dir/$name.md"
      # Only ever refresh a wrapper that is already installed: a missing one
      # was uninstalled or deliberately deleted, and resurrecting it would be
      # a surprise. A symlink is user-owned; leave it alone. A git-tracked
      # wrapper belongs to the repo — rewriting it would dirty a working tree
      # nobody asked us to touch.
      [ -f "$installed" ] || continue
      [ -L "$installed" ] && continue
      weave_command_tracked_by_git "$installed" && continue

      raw="$baseline_dir/$name.md.tmp.$$"
      curl -fsSL --max-time 15 "$url_base/$name.md" -o "$raw" 2>/dev/null || { rm -f "$raw"; continue; }
      # Shape check: every wrapper opens with YAML front matter, so a 404 page
      # or a truncated body can never be installed as a command.
      if [ ! -s "$raw" ] || [ "$(head -n 1 "$raw")" != "---" ]; then
        rm -f "$raw"
        continue
      fi

      prev="$baseline_dir/$name.md"
      if grep -q '{{SCOPE}}' "$raw" && [ "$scope_known" != "true" ]; then
        mv "$raw" "$prev" 2>/dev/null || rm -f "$raw"
        continue
      fi

      if [ -f "$prev" ]; then
        new_body="$(weave_render_command "$raw" "$scope_args")"
        prev_body="$(weave_render_command "$prev" "$scope_args")"
        installed_body="$(sed '/^<!-- weave-router managed command: .* -->$/d' "$installed" 2>/dev/null)" || installed_body=""
        if [ "$prev_body" = "$installed_body" ] && [ "$new_body" != "$installed_body" ]; then
          tmp="$installed.tmp.$$"
          if printf '%s\n' "$new_body" >"$tmp" 2>/dev/null; then
            mv "$tmp" "$installed" 2>/dev/null || rm -f "$tmp"
            ownership_file="$installed.weave-router"
            if [ ! -L "$ownership_file" ]; then
              printf 'weave-router managed command: %s\n%s\n' "$name" "$new_body" \
                >"$ownership_file" 2>/dev/null || true
            fi
          else
            rm -f "$tmp"
          fi
        fi
      fi
      mv "$raw" "$prev" 2>/dev/null || rm -f "$raw"
    done <<EOF
$(weave_installed_command_names "$cmd_dir")
EOF
  ) >/dev/null 2>&1 &
  disown 2>/dev/null || true
  return 0
}
weave_sync_commands 2>/dev/null || true

# ---------- org "hide terminal surfaces" gate ----------
#
# When the org has hidden the router's terminal surfaces, the statusline
# renders nothing: this prints no output and exits 0, leaving the slot blank.
# The setting comes from GET /v1/display-settings with the install's own
# router key, but the foreground path NEVER touches the network: it decides
# solely from a per-install cache file (TTL WEAVE_DISPLAY_SETTINGS_TTL_SECONDS,
# default 1h), and a missing or stale cache fails open — the statusline
# renders normally. When the cache is missing or stale a detached background
# refresh re-fetches the setting and rewrites the cache atomically, so the
# next turn picks up the fresh value; a refresh failure simply leaves the
# cache stale, which keeps failing open rather than pinning the gate closed.
#
# Claude Code runs the statusline every turn, so several invocations can see a
# stale cache at once. Letting each fetch independently is not safe: the
# responses can land out of order, and the loser's mv would replace a newer
# setting with an older one AND stamp it fresh, pinning the gate on a stale
# value for a full TTL. Refreshes are therefore serialized per cache key on a
# mkdir mutex (atomic everywhere; flock is absent on macOS), held across both
# the fetch and the write. A refresh that finds the mutex held exits rather
# than queueing — the in-flight one is at least as fresh as anything it would
# fetch, so waiting only to overwrite it is the bug. The one path that is not
# strictly exclusive is reclaiming a lock whose holder died; see below.
weave_hidden_gate() {
  command -v curl >/dev/null 2>&1 || return 1
  command -v jq >/dev/null 2>&1 || return 1

  local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/weave-router"
  mkdir -p "$cache_dir" 2>/dev/null || return 1
  local self="${BASH_SOURCE[0]:-$0}"
  local script_slug
  script_slug="$(printf '%s' "$self" | tr -c 'A-Za-z0-9._-' '_')"
  local cache="$cache_dir/display-settings${script_slug}"
  local ttl="${WEAVE_DISPLAY_SETTINGS_TTL_SECONDS:-3600}"

  local now mtime fresh="false"
  now="$(date +%s 2>/dev/null)" || now=0
  if [ -f "$cache" ]; then
    mtime="$(stat -c %Y "$cache" 2>/dev/null || stat -f %m "$cache" 2>/dev/null)" || mtime=0
    if [ -n "${mtime:-}" ] && [ "$mtime" -gt 0 ] && [ $(( now - mtime )) -lt "$ttl" ]; then
      fresh="true"
    fi
  fi

  # Foreground decision: only a fresh cache hides the surfaces. Anything else
  # (no cache, stale cache, unreadable cache) renders normally so a slow or
  # unreachable router can never stall a turn or wedge the statusline blank.
  if [ "$fresh" = "true" ]; then
    [ "$(cat "$cache" 2>/dev/null)" = "1" ]
    return
  fi

  # Background refresh for the next invocation. Resolve the router base URL
  # and key inside the subshell from the Claude Code settings the installer
  # wrote: project/--dir installs put both under <base>/.claude alongside
  # this script (key in settings.local.json), while a user-scope install
  # lives under ~/.weave and reads ~/.claude. Resolve relative to the
  # script's own location, falling back to user scope, so a project install
  # never reads (or leaks) the user-scope key. ANTHROPIC_BASE_URL and
  # WEAVE_ROUTER_BASE_URL may also be set in the environment. A file:// base
  # URL is the offline/test seam: curl reads it as the response body
  # directly, so the endpoint path is meaningless for it; real router URLs
  # (https) get /v1/display-settings appended.
  (
    exec </dev/null

    # Take the per-cache-key mutex, or bail. mkdir is the portable atomic
    # test-and-set. A crashed holder would otherwise block refreshes forever,
    # so a lock older than the fetch timeout is treated as abandoned and
    # reclaimed. Releasing from a trap covers every exit path below.
    lock="$cache.lock"
    if ! mkdir "$lock" 2>/dev/null; then
      lock_mtime="$(stat -c %Y "$lock" 2>/dev/null || stat -f %m "$lock" 2>/dev/null)" || lock_mtime=0
      lock_now="$(date +%s 2>/dev/null)" || lock_now=0
      if [ "${lock_mtime:-0}" -le 0 ] || [ $(( lock_now - lock_mtime )) -le 30 ]; then
        exit 0
      fi
      # Reclaiming an abandoned lock must not be delete-then-recreate. With
      # `rm -rf` + `mkdir`, refreshers that all see the same stale lock each
      # delete the next one's freshly created directory, so many end up holding
      # it at once (measured at 50 claimants: 4-11 concurrent holders) and their
      # writes can land out of order. Renaming is atomic, so only one racer can
      # move a given directory aside and the losers exit instead of clobbering
      # the winner (same measurement: 1-2). It is not a perfect mutex — a
      # straggler can still reclaim the new lock a winner just created, since
      # nothing distinguishes it from the stale one — but real refreshes arrive
      # one per turn rather than 50 at once, and the staleness threshold below
      # is what bounds the rest.
      dead="$lock.dead.$$"
      mv "$lock" "$dead" 2>/dev/null || exit 0
      rm -rf "$dead" 2>/dev/null
      mkdir "$lock" 2>/dev/null || exit 0
    fi
    trap 'rmdir "$lock" 2>/dev/null' EXIT

    self_dir="$(cd "$(dirname "$self")" 2>/dev/null && pwd)"
    settings_base="$HOME"
    case "$self_dir" in
      */.claude) settings_base="${self_dir%/.claude}" ;;
    esac
    settings="$settings_base/.claude/settings.json"
    local_settings="$settings_base/.claude/settings.local.json"
    base_url="${WEAVE_ROUTER_BASE_URL:-${ANTHROPIC_BASE_URL:-}}"
    key="${WEAVE_ROUTER_KEY:-}"
    if [ -z "$key" ] && [ -f "$settings" ]; then
      key="$(jq -r '.env.ANTHROPIC_CUSTOM_HEADERS // "" | split("\n")[] | select(startswith("X-Weave-Router-Key:")) | sub("^X-Weave-Router-Key:[[:space:]]*";"")' "$settings" 2>/dev/null | head -n1)"
    fi
    if [ -z "$key" ] && [ -f "$local_settings" ]; then
      key="$(jq -r '.env.ANTHROPIC_CUSTOM_HEADERS // "" | split("\n")[] | select(startswith("X-Weave-Router-Key:")) | sub("^X-Weave-Router-Key:[[:space:]]*";"")' "$local_settings" 2>/dev/null | head -n1)"
    fi
    if [ -z "$base_url" ] && [ -f "$settings" ]; then
      base_url="$(jq -r '.env.ANTHROPIC_BASE_URL // empty' "$settings" 2>/dev/null)"
    fi
    [ -n "$base_url" ] && [ -n "$key" ] || exit 0
    url="${base_url%/}"
    case "$url" in
      file://*) ;;
      *) url="$url/v1/display-settings" ;;
    esac
    body="$(curl -fsS --max-time 5 -H "X-Weave-Router-Key: $key" "$url" 2>/dev/null)" || exit 0
    hidden="$(printf '%s' "$body" | jq -r '.hide_terminal_surfaces // false' 2>/dev/null)"
    tmp="$cache.tmp.$$"
    if [ "$hidden" = "true" ]; then
      printf '1' >"$tmp" 2>/dev/null && mv "$tmp" "$cache" 2>/dev/null
    else
      printf '0' >"$tmp" 2>/dev/null && mv "$tmp" "$cache" 2>/dev/null
    fi
    rm -f "$tmp" 2>/dev/null
  ) >/dev/null 2>&1 &
  disown 2>/dev/null || true
  return 1
}

if weave_hidden_gate </dev/null; then
  exit 0
fi

input="$(cat)"
transcript_path="$(printf '%s' "$input" | jq -r '.transcript_path // empty')"
# Prefer model.id over display_name: pricing keys + the response model id in
# the transcript are canonical ids (e.g. claude-opus-4-7), while display_name
# is a human label ("Opus 4.7 (1M context)") that won't hit the pricing table,
# preventing a price comparison. id passes through normalize_model cleanly.
selected_display="$(printf '%s' "$input" | jq -r '.model.id // .model.display_name // "?"')"

# Normalize a model id to a pricing-table key. CC + the decisions log carry
# two flavors of annotation we don't want in the lookup:
#   * date suffix:    claude-opus-4-7-20260101  → claude-opus-4-7
#   * variant tag:    claude-opus-4-7[1m]       → claude-opus-4-7
# This estimate compares base model prices, not context-tier surcharges.
normalize_model() {
  printf '%s' "$1" | sed -E 's/\[[^]]*\]$//; s/-[0-9]{8}$//'
}

# USD per 1k tokens. Generated from internal/observability/otel/pricing.go
# (USD/1M there, ÷1000 here) by cmd/genprices. Do not hand-edit — run
# `make generate` after updating pricing.go.
# BEGIN_GENERATED_PRICES
prices='{
  "input": {
    "claude-fable-5":                   0.01,
    "claude-fable-5-1":                 0.01,
    "claude-haiku-4-5":                 0.001,
    "claude-opus-4-0":                  0.015,
    "claude-opus-4-1":                  0.015,
    "claude-opus-4-5":                  0.005,
    "claude-opus-4-6":                  0.005,
    "claude-opus-4-7":                  0.005,
    "claude-opus-4-8":                  0.005,
    "claude-opus-5":                    0.005,
    "claude-opus-5-5":                  0.004,
    "claude-sonnet-4-5":                0.003,
    "claude-sonnet-4-6":                0.003,
    "claude-sonnet-5":                  0.003,
    "claude-sonnet-5-5":                0.002,
    "deepseek/deepseek-v4-flash":       0.00014,
    "deepseek/deepseek-v4-pro":         0.00174,
    "deepseek/deepseek-v4-pro-0813":    0.00174,
    "deepseek/deepseek-v4.1-flash":     0.0002,
    "gemini-2.0-flash":                 0.0001,
    "gemini-2.0-flash-lite":            0.000075,
    "gemini-2.5-flash":                 0.0003,
    "gemini-2.5-flash-lite":            0.0001,
    "gemini-2.5-pro":                   0.00125,
    "gemini-3-flash-preview":           0.0005,
    "gemini-3-pro-preview":             0.002,
    "gemini-3.1-flash-lite-preview":    0.0001,
    "gemini-3.1-pro-preview":           0.002,
    "gemini-3.5-flash":                 0.0015,
    "gemini-3.5-flash-lite":            0.0003,
    "gemini-3.6-flash":                 0.0015,
    "gemini-3.7-flash":                 0.0015,
    "gemini-3.8-flash":                 0.0015,
    "google/gemma-4-26b-a4b-it":        0.00015,
    "gpt-4.1":                          0.002,
    "gpt-4.1-mini":                     0.0004,
    "gpt-4.1-nano":                     0.0001,
    "gpt-4o":                           0.0025,
    "gpt-4o-mini":                      0.00015,
    "gpt-5":                            0.0025,
    "gpt-5-chat":                       0.0025,
    "gpt-5-mini":                       0.0005,
    "gpt-5-nano":                       0.0001,
    "gpt-5.4":                          0.0025,
    "gpt-5.4-mini":                     0.00075,
    "gpt-5.4-nano":                     0.0002,
    "gpt-5.4-pro":                      0.03,
    "gpt-5.5":                          0.005,
    "gpt-5.5-mini":                     0.0005,
    "gpt-5.5-nano":                     0.00015,
    "gpt-5.5-pro":                      0.03,
    "gpt-5.6-luna":                     0.0002,
    "gpt-5.6-luna-pro":                 0.0002,
    "gpt-5.6-sol":                      0.004,
    "gpt-5.6-sol-pro":                  0.004,
    "gpt-5.6-terra":                    0.002,
    "gpt-6-astra":                      0.01,
    "gpt-6-luna":                       0.0001,
    "gpt-6-sol":                        0.002,
    "gpt-6.1-sol":                      0.002,
    "grok-4.5":                         0.002,
    "grok-4.6":                         0.002,
    "grok-4.7":                         0.002,
    "inclusionai/ling-3.0-flash":       0.00006,
    "minimax/minimax-m2.7":             0.0003,
    "minimax/minimax-m3":               0.0003,
    "mistralai/mistral-small-2603":     0.0002,
    "moonshotai/kimi-k2.5":             0.0006,
    "moonshotai/kimi-k2.6":             0.00095,
    "moonshotai/kimi-k2.7":             0.00095,
    "moonshotai/kimi-k3":               0.003,
    "muse-spark-1.3":                   0.00125,
    "qwen/qwen3-235b-a22b-2507":        0.0002266,
    "qwen/qwen3-30b-a3b-instruct-2507": 0.00015,
    "qwen/qwen3-coder":                 0.0009,
    "qwen/qwen3-coder-next":            0.0005,
    "qwen/qwen3-next-80b-a3b-instruct": 0.00015,
    "qwen/qwen3.5-flash-02-23":         0.00005,
    "qwen/qwen3.6-35b-a3b":             0.00015,
    "qwen/qwen3.7-plus":                0.0004,
    "qwen/qwen3.8-max":                 0.002,
    "xiaomi/mimo-v2.5-pro":             0.001,
    "xiaomi/mimo-v2.6-flash":           0.00014,
    "xiaomi/mimo-v2.6-pro":             0.000435,
    "z-ai/glm-5":                       0.001,
    "z-ai/glm-5.1":                     0.0014,
    "z-ai/glm-5.2":                     0.0014,
    "z-ai/glm-5.3":                     0.0014,
    "z-ai/glm-5.3-flash":               0.00015
  },
  "output": {
    "claude-fable-5":                   0.05,
    "claude-fable-5-1":                 0.05,
    "claude-haiku-4-5":                 0.005,
    "claude-opus-4-0":                  0.075,
    "claude-opus-4-1":                  0.075,
    "claude-opus-4-5":                  0.025,
    "claude-opus-4-6":                  0.025,
    "claude-opus-4-7":                  0.025,
    "claude-opus-4-8":                  0.025,
    "claude-opus-5":                    0.025,
    "claude-opus-5-5":                  0.02,
    "claude-sonnet-4-5":                0.015,
    "claude-sonnet-4-6":                0.015,
    "claude-sonnet-5":                  0.015,
    "claude-sonnet-5-5":                0.01,
    "deepseek/deepseek-v4-flash":       0.00028,
    "deepseek/deepseek-v4-pro":         0.00348,
    "deepseek/deepseek-v4-pro-0813":    0.00348,
    "deepseek/deepseek-v4.1-flash":     0.00099,
    "gemini-2.0-flash":                 0.0004,
    "gemini-2.0-flash-lite":            0.0003,
    "gemini-2.5-flash":                 0.0012,
    "gemini-2.5-flash-lite":            0.0004,
    "gemini-2.5-pro":                   0.005,
    "gemini-3-flash-preview":           0.002,
    "gemini-3-pro-preview":             0.008,
    "gemini-3.1-flash-lite-preview":    0.0004,
    "gemini-3.1-pro-preview":           0.008,
    "gemini-3.5-flash":                 0.009,
    "gemini-3.5-flash-lite":            0.0025,
    "gemini-3.6-flash":                 0.0075,
    "gemini-3.7-flash":                 0.0075,
    "gemini-3.8-flash":                 0.0075,
    "google/gemma-4-26b-a4b-it":        0.0006,
    "gpt-4.1":                          0.008,
    "gpt-4.1-mini":                     0.0016,
    "gpt-4.1-nano":                     0.0004,
    "gpt-4o":                           0.01,
    "gpt-4o-mini":                      0.0006,
    "gpt-5":                            0.01,
    "gpt-5-chat":                       0.01,
    "gpt-5-mini":                       0.002,
    "gpt-5-nano":                       0.0004,
    "gpt-5.4":                          0.015,
    "gpt-5.4-mini":                     0.0045,
    "gpt-5.4-nano":                     0.00125,
    "gpt-5.4-pro":                      0.18,
    "gpt-5.5":                          0.03,
    "gpt-5.5-mini":                     0.0025,
    "gpt-5.5-nano":                     0.0006,
    "gpt-5.5-pro":                      0.18,
    "gpt-5.6-luna":                     0.0012,
    "gpt-5.6-luna-pro":                 0.0012,
    "gpt-5.6-sol":                      0.02,
    "gpt-5.6-sol-pro":                  0.02,
    "gpt-5.6-terra":                    0.012,
    "gpt-6-astra":                      0.05,
    "gpt-6-luna":                       0.0005,
    "gpt-6-sol":                        0.01,
    "gpt-6.1-sol":                      0.01,
    "grok-4.5":                         0.006,
    "grok-4.6":                         0.006,
    "grok-4.7":                         0.006,
    "inclusionai/ling-3.0-flash":       0.00018,
    "minimax/minimax-m2.7":             0.0012,
    "minimax/minimax-m3":               0.0012,
    "mistralai/mistral-small-2603":     0.0006,
    "moonshotai/kimi-k2.5":             0.003,
    "moonshotai/kimi-k2.6":             0.004,
    "moonshotai/kimi-k2.7":             0.004,
    "moonshotai/kimi-k3":               0.015,
    "muse-spark-1.3":                   0.00425,
    "qwen/qwen3-235b-a22b-2507":        0.0009064,
    "qwen/qwen3-30b-a3b-instruct-2507": 0.0006,
    "qwen/qwen3-coder":                 0.0027,
    "qwen/qwen3-coder-next":            0.0012,
    "qwen/qwen3-next-80b-a3b-instruct": 0.0012,
    "qwen/qwen3.5-flash-02-23":         0.00015,
    "qwen/qwen3.6-35b-a3b":             0.001,
    "qwen/qwen3.7-plus":                0.0016,
    "qwen/qwen3.8-max":                 0.006,
    "xiaomi/mimo-v2.5-pro":             0.003,
    "xiaomi/mimo-v2.6-flash":           0.00028,
    "xiaomi/mimo-v2.6-pro":             0.00087,
    "z-ai/glm-5":                       0.0032,
    "z-ai/glm-5.1":                     0.0044,
    "z-ai/glm-5.2":                     0.0044,
    "z-ai/glm-5.3":                     0.0044,
    "z-ai/glm-5.3-flash":               0.0005
  },
  "cache_read": {
    "claude-fable-5":                   0.1,
    "claude-fable-5-1":                 0.025,
    "claude-haiku-4-5":                 0.1,
    "claude-opus-4-0":                  0.1,
    "claude-opus-4-1":                  0.1,
    "claude-opus-4-5":                  0.1,
    "claude-opus-4-6":                  0.1,
    "claude-opus-4-7":                  0.1,
    "claude-opus-4-8":                  0.1,
    "claude-opus-5":                    0.1,
    "claude-opus-5-5":                  0.05,
    "claude-sonnet-4-5":                0.1,
    "claude-sonnet-4-6":                0.1,
    "claude-sonnet-5":                  0.1,
    "claude-sonnet-5-5":                0.1,
    "deepseek/deepseek-v4-flash":       0.1,
    "deepseek/deepseek-v4-pro":         0.0862,
    "deepseek/deepseek-v4-pro-0813":    0.0862,
    "deepseek/deepseek-v4.1-flash":     0.03,
    "gemini-2.0-flash":                 0.25,
    "gemini-2.0-flash-lite":            0.25,
    "gemini-2.5-flash":                 0.1,
    "gemini-2.5-flash-lite":            0.1,
    "gemini-2.5-pro":                   0.1,
    "gemini-3-flash-preview":           0.1,
    "gemini-3-pro-preview":             0.1,
    "gemini-3.1-flash-lite-preview":    0.1,
    "gemini-3.1-pro-preview":           0.1,
    "gemini-3.5-flash":                 0.1,
    "gemini-3.5-flash-lite":            0.1,
    "gemini-3.6-flash":                 0.1,
    "gemini-3.7-flash":                 0.1,
    "gemini-3.8-flash":                 0.1,
    "google/gemma-4-26b-a4b-it":        0.1,
    "gpt-4.1":                          0.25,
    "gpt-4.1-mini":                     0.25,
    "gpt-4.1-nano":                     0.25,
    "gpt-4o":                           0.5,
    "gpt-4o-mini":                      0.5,
    "gpt-5":                            0.1,
    "gpt-5-chat":                       0.1,
    "gpt-5-mini":                       0.1,
    "gpt-5-nano":                       0.1,
    "gpt-5.4":                          0.1,
    "gpt-5.4-mini":                     0.1,
    "gpt-5.4-nano":                     0.1,
    "gpt-5.4-pro":                      1,
    "gpt-5.5":                          0.1,
    "gpt-5.5-mini":                     0.1,
    "gpt-5.5-nano":                     0.1,
    "gpt-5.5-pro":                      1,
    "gpt-5.6-luna":                     0.1,
    "gpt-5.6-luna-pro":                 0.1,
    "gpt-5.6-sol":                      0.1,
    "gpt-5.6-sol-pro":                  0.1,
    "gpt-5.6-terra":                    0.1,
    "gpt-6-astra":                      0.1,
    "gpt-6-luna":                       0.1,
    "gpt-6-sol":                        0.1,
    "gpt-6.1-sol":                      0.05,
    "grok-4.5":                         0.25,
    "grok-4.6":                         0.25,
    "grok-4.7":                         0.25,
    "inclusionai/ling-3.0-flash":       0.2,
    "minimax/minimax-m2.7":             0.2,
    "minimax/minimax-m3":               0.2,
    "mistralai/mistral-small-2603":     0.1,
    "moonshotai/kimi-k2.5":             0.5,
    "moonshotai/kimi-k2.6":             0.1684,
    "moonshotai/kimi-k2.7":             0.2,
    "moonshotai/kimi-k3":               0.1,
    "muse-spark-1.3":                   0.12,
    "qwen/qwen3-235b-a22b-2507":        0.5,
    "qwen/qwen3-30b-a3b-instruct-2507": 0.1684,
    "qwen/qwen3-coder":                 0.1684,
    "qwen/qwen3-coder-next":            0.5,
    "qwen/qwen3-next-80b-a3b-instruct": 0.5,
    "qwen/qwen3.5-flash-02-23":         0.1,
    "qwen/qwen3.6-35b-a3b":             0.1,
    "qwen/qwen3.7-plus":                0.2,
    "qwen/qwen3.8-max":                 0.125,
    "xiaomi/mimo-v2.5-pro":             0.1,
    "xiaomi/mimo-v2.6-flash":           0.02,
    "xiaomi/mimo-v2.6-pro":             0.00827586,
    "z-ai/glm-5":                       0.2,
    "z-ai/glm-5.1":                     0.18571428571428572,
    "z-ai/glm-5.2":                     0.18571428571428572,
    "z-ai/glm-5.3":                     0.18571428571428572,
    "z-ai/glm-5.3-flash":               0.2
  }
}'
# END_GENERATED_PRICES

transcript_model=""
has_pin_record="false"
last_pin_model=""
session_cost_difference=""

# This is a hypothetical comparison against the CURRENT selection, not
# realized router savings: the transcript does not record each turn's
# requested model or prove that any turn went through the router.

# Normalize the CC-side selection once for use in the jq math below.
requested_norm="$(normalize_model "$selected_display")"

if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
  # macOS ships `tail -r`, GNU coreutils ships `tac`. Either works to walk the
  # JSONL in reverse so we can grab the latest assistant turn.
  if command -v tac >/dev/null 2>&1; then reverse=(tac); else reverse=(tail -r); fi

  # CC stamps message.model = "<synthetic>" on assistant turns it generated
  # locally (errored requests, cancellations, tool-only stubs) instead of a
  # real model id. Show that as "failure" rather than leaking the internal
  # sentinel into the statusline.
  transcript_model="$("${reverse[@]}" "$transcript_path" 2>/dev/null \
    | jq -r 'select(.type=="assistant") | .message.model // empty' \
    | head -n 1 || true)"
  if [[ "$transcript_model" == "<synthetic>" ]]; then
    transcript_model="failure"
  else
    transcript_model="$(normalize_model "$transcript_model")"
  fi

  # Read the last /force-model acknowledgement in the transcript —
  # the only turns stamped message.model == "weave-router". The router emits
  # one whenever a pin changes state:
  #   * /force-model              → "force-model applied: <model> (<provider>) …"
  #   * /unforce-model            → "force-model cleared …"
  #   * loop / no-progress break  → "… clearing the session pin …" (expires the
  #                                  pin, including a user-forced one)
  #   * unrecognized model        → "… isn't a recognized model · keeping
  #                                  automatic routing" — a NO-OP: the prior
  #                                  pin, if any, is left untouched
  #   * listing (historical)      → "… pick a model by id …" / "no models are
  #                                  available to pin …" — also NO-OPs. The
  #                                  router no longer emits these, but a
  #                                  transcript written while it did still
  #                                  carries them, and they must not read as
  #                                  a clear or a live pin loses its [forced]
  # These persist on disk (the ingress stripper only scrubs them from upstream
  # requests). Classify each weave-router turn newest-first, skip the no-op
  # "rejected" acks, and let the latest real state change decide: an "applied"
  # marker records a pin (and names the model); a later clear removes it.
  # This history cannot establish the running client's route or current pin.
  # Restricting to weave-router turns keeps a normal reply that merely quotes
  # these phrases from flipping the tag. (A silent server-side TTL expiry emits
  # no turn and so can't be reflected here — the pin TTL outlives a session.)
  force_state="$("${reverse[@]}" "$transcript_path" 2>/dev/null \
    | jq -r 'select(.type=="assistant" and .message.model=="weave-router")
        | ([.message.content[]? | select(.type? == "text") | .text] | join(" ") | gsub("[\n\r]"; " ")) as $t
        | if ($t | test("force-model applied:")) then "APPLIED " + ($t | capture("force-model applied: (?<m>[^ ]+)").m)
          elif ($t | test("isn.t a recognized model")) then "REJECTED"
          elif ($t | test("pick a model by id|no models are available to pin")) then "REJECTED"
          else "CLEARED" end' 2>/dev/null \
    | grep -m1 -v '^REJECTED$' || true)"
  if [[ "$force_state" == APPLIED\ * ]]; then
    has_pin_record="true"
    last_pin_model="${force_state#APPLIED }"
  fi

  # Missing prices leave the comparison unknown without hiding the model.
  #
  # Dedup note: CC writes one JSONL entry per *content block* in an
  # assistant turn (text, text, tool_use → 3 entries), and every entry
  # carries the same `message.usage`. Summing per-entry triple-counts the
  # turn. We dedupe on (message.id, message.usage) before summing:
  #   * For native Anthropic upstreams message.id is unique per turn, so
  #     this collapses the content-block fan-out cleanly.
  #   * For non-Anthropic upstreams that round-trip through the router's
  #     translator, message.id can be a constant placeholder
  #     ("msg_translated"); usage still differs per turn (input_tokens
  #     grows), so the composite key keeps turns distinct. Two turns with
  #     byte-identical id AND usage would still collapse, but that's a
  #     genuine retry/duplicate we want to drop.
  session_cost_difference="$(
    jq -rs --argjson p "$prices" --arg requested "$requested_norm" '
      [.[] | select(.type=="assistant" and .message.model!="<synthetic>" and .message.model!="weave-router")] |
      unique_by([.message.id, .message.usage]) |
      .[] |
      .message as $m |
      ($m.model // "" | sub("\\[[^]]*\\]$"; "") | sub("-[0-9]{8}$"; "")) as $rm |
      {
        in:    ($m.usage.input_tokens // 0),
        out:   ($m.usage.output_tokens // 0),
        cwrt:  ($m.usage.cache_creation_input_tokens // 0),
        crd:   ($m.usage.cache_read_input_tokens // 0)
      } as $t |
      (if ($t.in + $t.out + $t.cwrt + $t.crd) == 0 then 0
       else
         ($p.input[$rm] // null)             as $rin  | ($p.output[$rm] // null)             as $rout |
         ($p.cache_read[$rm] // null)        as $rcr  |
         ($p.input[$requested] // null)      as $sin  | ($p.output[$requested] // null)      as $sout |
         ($p.cache_read[$requested] // null) as $scr  |
         if ($rin == null or $rout == null or $rcr == null or $sin == null or $sout == null or $scr == null) then null
         else
           (($t.in + 1.25 * $t.cwrt + $rcr * $t.crd) / 1000) as $routed_input_units |
           (($t.in + 1.25 * $t.cwrt + $scr * $t.crd) / 1000) as $requested_input_units |
           ($t.out / 1000)                                    as $output_units |
           ($routed_input_units * $rin + $output_units * $rout)       as $routed_cost |
           ($requested_input_units * $sin + $output_units * $sout)    as $requested_cost |
           ($requested_cost - $routed_cost)
         end
       end) as $savings |
      if $savings == null then "unknown" else $savings end
    ' "$transcript_path" 2>/dev/null \
    | awk 'BEGIN{s=0}
           {if ($1 == "unknown") comparison_unknown=1; else s+=$1}
           END{
             if (NR == 0 || comparison_unknown) printf "unknown"; else printf "%.4f", s;
           }'
  )" || true
fi

# ---------- refresh when a model isn't in the pricing table ----------
#
# A model missing from this copy's price table cannot be compared. Refresh
# off-schedule instead of waiting up to a week for the periodic check.
#
# Keyed on its own stamp per unpriced id: a model we never price (self-hosted,
# unrecognized) then costs one download per interval, not one per turn, and
# can't starve the periodic check.
weave_refresh_on_price_miss() {
  local candidates="" m
  for m in "$@"; do
    case "$m" in
      "" | "?" | failure | weave-router | "<synthetic>") continue ;;
    esac
    candidates="${candidates}${m}"$'\n'
  done
  [ -n "$candidates" ] || return 0

  # A malformed $prices emits nothing here, which fails closed (no refresh)
  # rather than re-downloading every turn.
  local missing
  missing="$(printf '%s' "$candidates" \
    | jq -rR --argjson p "$prices" \
        'select($p.input[.] == null or $p.output[.] == null)' 2>/dev/null \
    | head -n 1)" || return 0
  [ -n "$missing" ] || return 0

  local model_slug
  model_slug="$(printf '%s' "$missing" | tr -c 'A-Za-z0-9._-' '_')"
  weave_self_refresh ".miss.${model_slug}" 1
}
weave_refresh_on_price_miss "$requested_norm" "$transcript_model" 2>/dev/null || true

# Brand color (#FF6C47) on terminals that grok 24-bit truecolor — that's
# every modern one (iTerm2, Apple Terminal, vscode, ghostty, alacritty,
# wezterm, kitty). Falls back gracefully on any escape-stripping terminal.
brand=$'\033[38;2;255;108;71mWEAVE ROUTER\033[0m'

# Format helpers.
fmt_money() {
  awk -v v="$1" 'BEGIN{
    if (v == "" || v+0 == 0)        { printf "$0.00";        exit }
    if (v+0 < 0.005 && v+0 > -0.005){ printf "%s<$0.01", (v+0 < 0 ? "-" : ""); exit }
    if (v+0 < 0)                    { printf "-$%.2f", -v+0; exit }
    printf "$%.2f", v
  }'
}

printf '%s' "$brand"
if [[ "$transcript_model" == "failure" ]]; then
  printf ' · last response failed'
elif [[ "$transcript_model" == "weave-router" ]]; then
  printf ' · control acknowledgement'
elif [[ -n "$transcript_model" ]]; then
  printf ' · response model: %s' "$transcript_model"
fi
if [[ -n "$selected_display" && "$selected_display" != "?" ]]; then
  printf ' · selected: %s' "$selected_display"
fi
if [[ -n "$session_cost_difference" && "$session_cost_difference" != "unknown" && "$transcript_model" != "failure" && "$transcript_model" != "weave-router" ]]; then
  printf ' · est. cost difference %s' "$(fmt_money "$session_cost_difference")"
fi
if [[ "$has_pin_record" == "true" ]]; then
  printf ' · last pin: %s' "$last_pin_model"
fi
