#!/usr/bin/env node
/**
 * SmartStack status line for Claude Code.
 *
 * Deployed by `ss install` to <claudeDir>/scripts/statusline/index.js and wired
 * into settings.json as:
 *   "statusLine": { "type": "command", "command": "node \"<path>\"", "padding": 0 }
 *
 * Claude Code pipes a session JSON object on stdin (model, workspace, cost,
 * context_window, rate_limits, ...) and renders everything this script writes to
 * stdout. Renders a 2-line status line:
 *   L1: CTX <bar> N% │ 5h <bar> N% (Nh) │ 7d <bar> N% (Nj)
 *   L2: <model> │ <dir> │ ⎇ <branch> │ +added/-removed │ $cost
 *
 * Zero dependencies — runs on the Node that ships with the CLI. Degrades to a
 * minimal label when stdin is empty or unparseable, and silently drops any
 * segment whose data is absent, so it can never break the UI.
 *
 * Schema + rules: https://code.claude.com/docs/en/statusline
 *
 * SOURCE OF TRUTH: edit this file in the CLI repo (templates/scripts/statusline/).
 * The copy under ~/.claude/ is overwritten on every `ss install`/`ss upgrade`.
 */
'use strict';

const fs = require('fs');
const path = require('path');

// ---- ANSI (honors NO_COLOR) ------------------------------------------------
const NO_COLOR = !!process.env.NO_COLOR;
const paint = (code, s) => (NO_COLOR ? String(s) : `\x1b[${code}m${s}\x1b[0m`);
const dim = (s) => paint('2', s);
const cyan = (s) => paint('36', s);
const blue = (s) => paint('34', s);
const magenta = (s) => paint('35', s);
const green = (s) => paint('32', s);
const red = (s) => paint('31', s);
// 256-color palette for the L1 gauges (matches the previous PowerShell line).
const green2 = (s) => paint('38;5;46', s);
const orange = (s) => paint('38;5;208', s);
const red2 = (s) => paint('38;5;196', s);
const SEP = dim(' │ '); // " │ "

// Color thresholds (percentage used).
const ctxColor = (p) => (p >= 40 ? red2 : p >= 30 ? orange : green2);
const rateColor = (p) => (p >= 80 ? red2 : p >= 50 ? orange : green2);

// Rate-limit window lengths (seconds), for pace coloring.
const WINDOW_SEC = { five_hour: 5 * 3600, seven_day: 7 * 86400 };

// Pace color: project end-of-window usage at current consumption rate.
// Green < 80%, orange 80-120%, red > 120% (20% margin absorbs normal variance).
function paceColor(usedPct, resetsAt, windowSec) {
  if (typeof resetsAt !== 'number' || !windowSec) return rateColor(usedPct);
  const remainingPct = Math.max(0, Math.min(100, ((resetsAt * 1000 - Date.now()) / 1000 / windowSec) * 100));
  const elapsedPct = 100 - remainingPct;
  if (elapsedPct < 5) return rateColor(usedPct);
  const projected = (usedPct / elapsedPct) * 100;
  if (projected < 80) return green2;
  return projected < 120 ? orange : red2;
}

// ---- gauge + reset formatting ----------------------------------------------
const BAR_W = 10;
function bar(pct, color) {
  let filled = Math.floor((pct / 100) * BAR_W);
  if (filled > BAR_W) filled = BAR_W;
  if (filled < 0) filled = 0;
  return color('█'.repeat(filled) + '░'.repeat(BAR_W - filled));
}

// Hours / days until a Unix-seconds reset timestamp. Empty if past/absent.
function resetHours(sec) {
  if (typeof sec !== 'number') return '';
  const msLeft = sec * 1000 - Date.now();
  if (msLeft <= 0) return '';
  return `${(msLeft / 3600000).toFixed(1)}h`;
}
function resetDays(sec) {
  if (typeof sec !== 'number') return '';
  const msLeft = sec * 1000 - Date.now();
  if (msLeft <= 0) return '';
  return `${(msLeft / 86400000).toFixed(1)}j`;
}

// ---- git branch (zero-subprocess, reads .git directly) ---------------------
function readFirstLine(file) {
  try {
    return fs.readFileSync(file, 'utf8').split('\n')[0].trim();
  } catch {
    return null;
  }
}

function resolveGitDir(startDir) {
  let dir = startDir;
  for (;;) {
    const dotGit = path.join(dir, '.git');
    try {
      const st = fs.statSync(dotGit);
      if (st.isDirectory()) return dotGit;
      if (st.isFile()) {
        // Worktree / submodule: `.git` is a file containing `gitdir: <path>`.
        const m = (readFirstLine(dotGit) || '').match(/^gitdir:\s*(.+)$/);
        return m ? path.resolve(dir, m[1].trim()) : null;
      }
    } catch {
      // no .git entry here — walk up
    }
    const parent = path.dirname(dir);
    if (parent === dir) return null; // reached filesystem root
    dir = parent;
  }
}

function gitBranch(cwd) {
  if (!cwd) return null;
  const gitDir = resolveGitDir(cwd);
  if (!gitDir) return null;
  const head = readFirstLine(path.join(gitDir, 'HEAD'));
  if (!head) return null;
  const ref = head.match(/^ref:\s*refs\/heads\/(.+)$/);
  if (ref) return ref[1];
  return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 7) : null; // detached HEAD
}

// ---- render ----------------------------------------------------------------
// L1: context + rate-limit gauges. Each segment is dropped when its data is
// absent, so older Claude Code builds (no rate_limits) degrade cleanly.
function renderLine1(d) {
  const out = [];

  const ctx = d.context_window;
  if (ctx && typeof ctx.used_percentage === 'number') {
    const p = ctx.used_percentage;
    const col = ctxColor(p);
    out.push(`CTX ${bar(p, col)} ${col(Math.round(p) + '%')}`);
  } else if (d.exceeds_200k_tokens) {
    out.push(red2('CTX 200k+'));
  }

  const rl = d.rate_limits || {};
  // suffix(resets_at) -> dimmed text after the gauge (parens included), or ''.
  const gauge = (label, node, windowSec, suffix) => {
    if (!node || typeof node.used_percentage !== 'number') return;
    const p = node.used_percentage;
    const col = paceColor(p, node.resets_at, windowSec);
    let seg = `${label} ${bar(p, col)} ${col(Math.round(p) + '%')}`;
    const suf = suffix ? suffix(node.resets_at) : '';
    if (suf) seg += ' ' + dim(suf);
    out.push(seg);
  };
  // 5h: hours until reset. 7d (weekly): days until reset. Both a bare value, no label.
  gauge('5h', rl.five_hour, WINDOW_SEC.five_hour, (s) => { const v = resetHours(s); return v ? `(${v})` : ''; });
  gauge('7d', rl.seven_day, WINDOW_SEC.seven_day, (s) => { const v = resetDays(s); return v ? `(${v})` : ''; });

  return out;
}

// L2: session meta — model │ dir │ branch │ lines changed │ cost.
function renderLine2(d) {
  const out = [];

  const model = d.model && d.model.display_name;
  if (model) out.push(cyan(model));

  const dir = (d.workspace && d.workspace.current_dir) || d.cwd || null;
  if (dir) out.push(blue(path.basename(dir)));

  const branch = (d.workspace && d.workspace.git_worktree) || gitBranch(dir);
  if (branch) out.push(magenta(`⎇ ${branch}`)); // "⎇ <branch>"

  const cost = d.cost || {};
  const added = cost.total_lines_added || 0;
  const removed = cost.total_lines_removed || 0;
  if (added || removed) {
    out.push(`${green('+' + added)}${dim('/')}${red('-' + removed)}`);
  }

  if (typeof cost.total_cost_usd === 'number' && cost.total_cost_usd > 0) {
    const usd = cost.total_cost_usd;
    out.push(dim(usd < 1 ? `$${usd.toFixed(3)}` : `$${usd.toFixed(2)}`));
  }

  return out;
}

function render(data) {
  const d = data && typeof data === 'object' ? data : {};
  const lines = [];
  const l1 = renderLine1(d);
  const l2 = renderLine2(d);
  if (l1.length) lines.push(l1.join(SEP));
  if (l2.length) lines.push(l2.join(SEP));
  return lines.length ? lines.join('\n') : dim('SmartStack');
}

// ---- stdin -> stdout -------------------------------------------------------
// Manual run (no pipe): stdin is a TTY and never ends — render an empty line
// immediately instead of hanging.
if (process.stdin.isTTY) {
  process.stdout.write(render({}));
  process.exit(0);
}

let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
  input += chunk;
});
process.stdin.on('end', () => {
  let data = {};
  try {
    data = JSON.parse(input || '{}');
  } catch {
    /* keep empty */
  }
  try {
    process.stdout.write(render(data));
  } catch {
    process.stdout.write(dim('SmartStack'));
  }
});
process.stdin.on('error', () => process.stdout.write(dim('SmartStack')));
