#!/usr/bin/env bash

# How much of the Claude subscription this account has used.
#
# Writes one display-ready record to $XDG_STATE_HOME/panama/agent-usage.json.
# The bar widget only ever reads that file, so adding a second agent later is a
# collector rather than a change to any QML.
#
# ── What this deliberately does NOT do ───────────────────────────────────────
#
# It never refreshes the OAuth token, and it never writes to
# ~/.claude/.credentials.json.
#
# That token expires about hourly and Claude Code refreshes it on demand. If
# this refreshed it too, two processes would be rotating one credential: a
# refresh that rotates the refresh token invalidates the other holder's copy,
# and the failure mode is being silently logged out of Claude Code by a status
# widget. No bar indicator is worth that.
#
# So this reads the token, uses it if it is still valid, and reports
# "unavailable" if it is not. In practice that covers the case that matters --
# while you are actually using Claude Code the token is fresh, and while you
# are not, there is nothing to watch.
#
# ── The token ────────────────────────────────────────────────────────────────
#
# Never reaches argv. `curl --config -` takes the Authorization header on
# stdin, because a header passed as an argument is world-readable in
# /proc/<pid>/cmdline for as long as the request takes -- the same rule
# panama-pick follows for passwords and panama-sudo for the MOK hash.
#
# Never reaches the output either. The record below carries percentages and
# timestamps and nothing else; the widget has no business seeing a credential
# and neither does anyone reading the state file.

set -uo pipefail

CREDENTIALS="${PANAMA_AGENT_CREDENTIALS:-$HOME/.claude/.credentials.json}"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
OUTPUT="$STATE_DIR/agent-usage.json"
ENDPOINT="${PANAMA_AGENT_USAGE_ENDPOINT:-https://api.anthropic.com/api/oauth/usage}"

mkdir -p "$STATE_DIR"

# Written whatever happens, so the widget can distinguish "no data yet" from
# "collector never ran" and hide itself for the right reason.
emit() {
    local status="$1" detail="${2:-}" body="${3:-null}"
    local tmp
    tmp="$(mktemp "$OUTPUT.XXXXXX")"
    jq -n --arg status "$status" --arg detail "$detail" \
          --argjson usage "$body" --arg at "$(date -Is)" \
          '{status: $status, detail: $detail, collectedAt: $at, usage: $usage}' \
        >"$tmp" 2>/dev/null || printf '{"status":"error","detail":"could not write","usage":null}' >"$tmp"
    mv "$tmp" "$OUTPUT"
}

command -v jq >/dev/null 2>&1 || exit 0
[[ -r "$CREDENTIALS" ]] || { emit unavailable "Claude Code is not signed in on this machine."; exit 0; }

expires="$(jq -r '.claudeAiOauth.expiresAt // 0' "$CREDENTIALS" 2>/dev/null)"
[[ "$expires" =~ ^[0-9]+$ ]] || expires=0
now="$(( $(date +%s) * 1000 ))"

# Thirty seconds of headroom: a token about to expire will have expired by the
# time the request lands, and a 401 is a worse answer than an honest wait.
if (( expires <= now + 30000 )); then
    emit stale "Waiting for Claude Code to refresh its session."
    exit 0
fi

config="$(mktemp)"
cleanup() { rm -f "$config"; }
trap cleanup EXIT
chmod 600 "$config"

jq -r '"header = \"Authorization: Bearer \(.claudeAiOauth.accessToken)\"\nheader = \"anthropic-beta: oauth-2025-04-20\"\nsilent\nshow-error"' \
    "$CREDENTIALS" >"$config" 2>/dev/null \
    || { emit unavailable "Could not read the Claude Code session."; exit 0; }

response="$(curl --max-time 10 --config "$config" "$ENDPOINT" 2>/dev/null)" || {
    emit unavailable "Could not reach the usage service."
    exit 0
}
rm -f "$config"

jq -e . >/dev/null 2>&1 <<<"$response" || { emit unavailable "The usage service returned something unreadable."; exit 0; }
if jq -e '.error' >/dev/null 2>&1 <<<"$response"; then
    emit unavailable "$(jq -r '.error.message // "The usage service refused the request."' <<<"$response")"
    exit 0
fi

# Reshaped into a small, stable record rather than passed through, so the
# widget does not depend on the shape of an endpoint nobody documents. Every
# field is optional: an endpoint that stops reporting one should cost that
# number, not the whole indicator.
usage="$(jq -c '
    # The endpoint reports utilisation as a percentage already -- 15 means 15%.
    # This multiplied by 100 on the assumption it was a 0..1 fraction, which is
    # how the bar came to read 1500%. Clamped as well as rounded, because a
    # readout is a number you glance at and trust; one that can exceed 100
    # teaches you not to.
    def pct: if type == "number" then ([[(. | round), 0] | max, 100] | min) else null end;
    {
        tier: (.rate_limit_tier // .rateLimitTier // null),
        subscription: (.subscription_type // .subscriptionType // null),
        fiveHour: {
            used: ((.five_hour.utilization // .fiveHour.utilization // null) | pct),
            resetsAt: (.five_hour.resets_at // .fiveHour.resetsAt // null)
        },
        week: {
            used: ((.seven_day.utilization // .week.utilization // null) | pct),
            resetsAt: (.seven_day.resets_at // .week.resetsAt // null)
        }
    }' <<<"$response" 2>/dev/null)"

[[ -n "$usage" ]] || { emit unavailable "The usage service returned an unfamiliar shape."; exit 0; }
emit ok "" "$usage"
