#!/usr/bin/env node
/**
 * scripts/statusline.mjs — Claude Code statusLine callback for web-terminal (B2).
 *
 * Claude Code pipes statusLine JSON to stdin when this script is configured
 * as a statusLine handler. The script:
 *   1. POSTs the raw JSON to $WEBTERM_STATUSLINE_URL with X-Webterm-Session header
 *      (server parses + broadcasts to attached clients via parseStatusLine)
 *   2. Prints a one-line summary (ctx% · $cost · model) to stdout for Claude's status bar
 *
 * Outside web-terminal ($WEBTERM_STATUSLINE_URL not set) → prints summary only, no POST.
 * Never throws; all network errors are silently absorbed.
 *
 * Field mapping from raw statusLine JSON (R0-confirmed, see src/types.ts StatusTelemetry):
 *   context_window.used_percentage → contextUsedPct
 *   cost.total_cost_usd            → costUsd
 *   model.display_name (or model)  → model
 *
 * Installed via scripts/setup-hooks.mjs (T-hooks-installer).
 * Idempotent marker: WEBTERM_STATUSLINE_URL env var set by spawn env (T-spawn-env).
 */

import { fileURLToPath } from 'node:url'

/** Safety cap on stdin reads (64 KB). */
const MAX_STDIN_BYTES = 64 * 1024

/** Max length for model display name (prevents runaway strings). */
const MAX_MODEL_LEN = 100

/**
 * Read all stdin into a string (up to maxBytes). Never throws.
 *
 * @param {number} [maxBytes]
 * @returns {Promise<string>}
 */
export async function readStdin(maxBytes = MAX_STDIN_BYTES) {
  return new Promise((resolve) => {
    /** @type {Buffer[]} */
    const chunks = []
    let total = 0
    process.stdin.on('data', (/** @type {Buffer} */ chunk) => {
      total += chunk.length
      if (total <= maxBytes) chunks.push(chunk)
    })
    process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
    process.stdin.on('error', () => resolve(''))
  })
}

/**
 * Parse the raw Claude Code statusLine JSON into display fields.
 * Tolerant: missing/invalid fields → undefined, never throws.
 *
 * @param {string} raw - raw stdin JSON string
 * @returns {{ contextUsedPct?: number; costUsd?: number; model?: string } | null}
 *   Returns null for unparseable or non-object JSON; returns object (with possible
 *   undefined fields) for valid JSON objects.
 */
export function parseRaw(raw) {
  /** @type {unknown} */
  let json
  try {
    json = JSON.parse(raw)
  } catch {
    return null
  }
  if (typeof json !== 'object' || json === null || Array.isArray(json)) return null

  /** @type {Record<string, unknown>} */
  const obj = /** @type {Record<string, unknown>} */ (json)

  /**
   * Extract a finite number from an unknown value.
   * @param {unknown} v
   * @returns {number | undefined}
   */
  const safeNum = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : undefined)

  // context_window.used_percentage
  const ctxWin =
    obj['context_window'] != null &&
    typeof obj['context_window'] === 'object' &&
    !Array.isArray(obj['context_window'])
      ? /** @type {Record<string, unknown>} */ (obj['context_window'])
      : null
  const contextUsedPct = ctxWin ? safeNum(ctxWin['used_percentage']) : undefined

  // cost.total_cost_usd
  const costObj =
    obj['cost'] != null &&
    typeof obj['cost'] === 'object' &&
    !Array.isArray(obj['cost'])
      ? /** @type {Record<string, unknown>} */ (obj['cost'])
      : null
  const costUsd = costObj ? safeNum(costObj['total_cost_usd']) : undefined

  // model.display_name OR model (string fallback)
  const rawModel =
    obj['model'] != null &&
    typeof obj['model'] === 'object' &&
    !Array.isArray(obj['model'])
      ? /** @type {Record<string, unknown>} */ (obj['model'])['display_name']
      : obj['model']
  const model =
    typeof rawModel === 'string' && rawModel.length > 0
      ? rawModel.slice(0, MAX_MODEL_LEN)
      : undefined

  return { contextUsedPct, costUsd, model }
}

/**
 * Build the one-line summary for Claude's status bar.
 * Format: "ctx:45% · $0.53 · claude-opus-4-5"
 * Returns empty string if no fields are available.
 *
 * @param {{ contextUsedPct?: number; costUsd?: number; model?: string } | null} parsed
 * @returns {string}
 */
export function buildSummary(parsed) {
  if (!parsed) return ''
  /** @type {string[]} */
  const parts = []
  if (typeof parsed.contextUsedPct === 'number') {
    parts.push(`ctx:${Math.round(parsed.contextUsedPct)}%`)
  }
  if (typeof parsed.costUsd === 'number') {
    parts.push(`$${parsed.costUsd.toFixed(2)}`)
  }
  if (typeof parsed.model === 'string' && parsed.model.length > 0) {
    parts.push(parsed.model)
  }
  return parts.join(' · ')
}

/**
 * POST the raw JSON body to the statusLine URL with the session header.
 * Resolves when complete or on any error. Never throws.
 * Uses AbortController to enforce a deadline.
 *
 * @param {string} url - $WEBTERM_STATUSLINE_URL (loopback http://127.0.0.1:<port>/hook/status)
 * @param {string} sessionId - $WEBTERM_SESSION (identifies the Claude session)
 * @param {string} body - raw stdin JSON to forward verbatim
 * @param {number} [timeoutMs] - abort after this many ms (default 5000)
 * @returns {Promise<void>}
 */
export async function postToServer(url, sessionId, body, timeoutMs = 5000) {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), timeoutMs)
  try {
    await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Webterm-Session': sessionId,
      },
      body,
      signal: controller.signal,
    })
  } catch {
    // silent: server down, wrong URL, timeout, outside web-terminal — all absorbed
  } finally {
    clearTimeout(timer)
  }
}

// ── Main execution (only when run directly, not when imported for testing) ──
if (process.argv[1] === fileURLToPath(import.meta.url)) {
  void (async () => {
    const raw = await readStdin()
    const parsed = parseRaw(raw)
    const summary = buildSummary(parsed)

    // Print summary to stdout — Claude uses this as the status line text.
    // Empty summary (no data or unparseable) → print nothing (Claude keeps its default).
    if (summary) process.stdout.write(summary + '\n')

    // POST to server only when inside web-terminal (env var injected by spawn env).
    const url = process.env['WEBTERM_STATUSLINE_URL']
    const sessionId = process.env['WEBTERM_SESSION'] ?? ''
    if (url) {
      await postToServer(url, sessionId, raw)
    }
  })()
}
