/**
 * @fileoverview Pure parsing + formatting of Claude Code statusline telemetry.
 *
 * Claude Code (v2.1.80+) pipes a JSON blob to a configured `statusLine.command`
 * on each render. On Pro/Max subscriptions that blob carries a `rate_limits`
 * object with the 5-hour rolling and 7-day weekly plan windows. The
 * Codeman-managed statusLine exporter (see `hooks-config.generateStatusLineCommand`)
 * POSTs that blob to `/api/status-telemetry`; these helpers normalize the subset
 * Codeman displays and format the compact in-terminal footer string.
 *
 * Confirmed schema (empirically captured, CC 2.1.177, Claude Max — see
 * `docs/usage-limits-display-plan.md`):
 *   rate_limits.{five_hour,seven_day}.{used_percentage: number 0-100,
 *                                      resets_at: number EPOCH-SECONDS}
 * Only those two windows exist (no Opus-weekly field). `rate_limits` is absent
 * before the first API response and for non-subscriber auth — both yield null.
 *
 * All functions are pure for testability. See `test/usage-telemetry.test.ts`.
 *
 * @module usage-telemetry
 */
function clampPct(n) {
    if (!Number.isFinite(n))
        return 0;
    return Math.max(0, Math.min(100, n));
}
function parseWindow(w) {
    if (!w || typeof w.used_percentage !== 'number' || typeof w.resets_at !== 'number')
        return undefined;
    if (!Number.isFinite(w.resets_at) || w.resets_at <= 0)
        return undefined;
    return { usedPercentage: clampPct(w.used_percentage), resetAt: Math.round(w.resets_at * 1000) };
}
/**
 * Normalize a raw statusline payload to the telemetry Codeman displays. Returns
 * null when there is no plan-limit data to show (pre-first-response or a
 * non-subscriber account) so the caller can skip broadcasting.
 */
export function parseStatusTelemetry(data) {
    if (!data)
        return null;
    const fiveHour = parseWindow(data.rate_limits?.five_hour);
    const sevenDay = parseWindow(data.rate_limits?.seven_day);
    if (!fiveHour && !sevenDay)
        return null;
    const t = {};
    if (fiveHour)
        t.fiveHour = fiveHour;
    if (sevenDay)
        t.sevenDay = sevenDay;
    if (typeof data.context_window?.used_percentage === 'number') {
        t.contextUsedPercentage = clampPct(data.context_window.used_percentage);
    }
    if (typeof data.cost?.total_cost_usd === 'number' && Number.isFinite(data.cost.total_cost_usd)) {
        t.costUsd = data.cost.total_cost_usd;
    }
    if (typeof data.model?.display_name === 'string' && data.model.display_name) {
        t.modelDisplayName = data.model.display_name.slice(0, 60);
    }
    return t;
}
/** Group a non-negative integer with thousands separators: 562411 → "562,411". */
function withCommas(n) {
    return Math.max(0, Math.round(n))
        .toString()
        .replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
/** Extract current-session status (footer) from the raw payload. */
export function parseSessionStatus(data) {
    if (!data)
        return null;
    const s = {};
    if (typeof data.model?.display_name === 'string' && data.model.display_name) {
        s.modelDisplayName = data.model.display_name.slice(0, 60);
    }
    const cw = data.context_window;
    if (typeof cw?.total_input_tokens === 'number' && Number.isFinite(cw.total_input_tokens)) {
        s.inputTokens = Math.max(0, cw.total_input_tokens);
    }
    if (typeof cw?.total_output_tokens === 'number' && Number.isFinite(cw.total_output_tokens)) {
        s.outputTokens = Math.max(0, cw.total_output_tokens);
    }
    if (typeof cw?.used_percentage === 'number') {
        s.contextUsedPercentage = clampPct(cw.used_percentage);
    }
    return Object.keys(s).length ? s : null;
}
/**
 * Format the in-terminal statusline footer: the CURRENT SESSION's status —
 * `Opus 4.8 (1M context)  in:562,411 out:1,188  ctx:56%` — NOT the plan limits,
 * which live in the Codeman header chip. Claude requires a statusLine command to
 * emit the rate_limits JSON at all, so this is what that command prints back.
 */
export function formatSessionStatusText(s) {
    if (!s)
        return 'codeman';
    const groups = [];
    if (s.modelDisplayName)
        groups.push(s.modelDisplayName);
    const tok = [];
    if (s.inputTokens != null)
        tok.push(`in:${withCommas(s.inputTokens)}`);
    if (s.outputTokens != null)
        tok.push(`out:${withCommas(s.outputTokens)}`);
    if (tok.length)
        groups.push(tok.join(' '));
    if (s.contextUsedPercentage != null)
        groups.push(`ctx:${Math.round(clampPct(s.contextUsedPercentage))}%`);
    return groups.length ? groups.join('  ') : 'codeman';
}
/**
 * Stable signature for change-detection — the statusline fires on every
 * assistant message, so the route only rebroadcasts when this value changes.
 *
 * Keys on EXACTLY the values the header chip displays: the two windows' ROUNDED
 * percentages (the chip renders `Math.round`) + their reset times. Deliberately
 * excludes contextUsedPercentage / costUsd / modelDisplayName — none are shown
 * in the chip, and contextUsedPercentage in particular drifts on every assistant
 * message, which would defeat the dedup and fan out a redundant SSE broadcast +
 * localStorage write + identical chip re-render each time.
 */
export function telemetrySignature(t) {
    return JSON.stringify([
        t.fiveHour ? Math.round(t.fiveHour.usedPercentage) : null,
        t.fiveHour?.resetAt ?? null,
        t.sevenDay ? Math.round(t.sevenDay.usedPercentage) : null,
        t.sevenDay?.resetAt ?? null,
    ]);
}
//# sourceMappingURL=usage-telemetry.js.map