/**
 * src/http/statusline.ts — parse a Claude Code statusLine JSON body (B2).
 *
 * Pure function, never throws. Maps the statusLine stdin JSON (R0-confirmed
 * field names, see StatusTelemetry comment in types.ts) into a StatusTelemetry.
 *
 * Security: SEC-M7 — unknown input narrowed field-by-field; string lengths capped.
 *
 * Field mapping (JSON → StatusTelemetry):
 *   context_window.used_percentage → contextUsedPct
 *   cost.total_cost_usd            → costUsd
 *   cost.total_lines_added         → linesAdded
 *   cost.total_lines_removed       → linesRemoved
 *   model.display_name             → model
 *   effort.level                   → effort
 *   pr                             → pr (same shape, required: number+url)
 *   rate_limits                    → rate (fiveHourPct, sevenDayPct)
 */

import type { StatusTelemetry } from '../types.js';

// String length limits (SEC-M7 — prevent DoS through overly long strings)
const MODEL_MAX_LEN = 200;
const EFFORT_MAX_LEN = 100;
const URL_MAX_LEN = 2000;
const REVIEW_STATE_MAX_LEN = 100;

/**
 * Parse the body of a POST /hook/status request into a StatusTelemetry.
 *
 * @param body - raw untrusted JSON body (unknown type)
 * @returns StatusTelemetry with at always set to Date.now(), or null when
 *          body is not a plain object. Missing/dirty fields → undefined.
 *          Never throws.
 */
export function parseStatusLine(body: unknown): StatusTelemetry | null {
  // Non-objects (including null and arrays) → null
  if (body === null || typeof body !== 'object' || Array.isArray(body)) return null;

  const b = body as Record<string, unknown>;

  const contextUsedPct = safeFiniteNumber(nestedValue(b, 'context_window', 'used_percentage'));

  const costObj = asObjectOrNull(b['cost']);
  const costUsd = safeFiniteNumber(costObj?.['total_cost_usd']);
  const linesAdded = safeFiniteNumber(costObj?.['total_lines_added']);
  const linesRemoved = safeFiniteNumber(costObj?.['total_lines_removed']);

  const modelObj = asObjectOrNull(b['model']);
  const model = safeBoundedString(modelObj?.['display_name'], MODEL_MAX_LEN);

  const effortObj = asObjectOrNull(b['effort']);
  const effort = safeBoundedString(effortObj?.['level'], EFFORT_MAX_LEN);

  const pr = parsePr(b['pr']);
  const rate = parseRate(b['rate_limits']);

  return {
    at: Date.now(),
    ...(contextUsedPct !== undefined && { contextUsedPct }),
    ...(costUsd !== undefined && { costUsd }),
    ...(linesAdded !== undefined && { linesAdded }),
    ...(linesRemoved !== undefined && { linesRemoved }),
    ...(model !== undefined && { model }),
    ...(effort !== undefined && { effort }),
    ...(pr !== undefined && { pr }),
    ...(rate !== undefined && { rate }),
  };
}

// ─── Private helpers ─────────────────────────────────────────────────────────

/** Get obj[key1][key2], or undefined if either level is not a plain object. */
function nestedValue(obj: Record<string, unknown>, key1: string, key2: string): unknown {
  const inner = asObjectOrNull(obj[key1]);
  return inner !== null ? inner[key2] : undefined;
}

/** Return v if it is a finite number; otherwise undefined. */
function safeFiniteNumber(v: unknown): number | undefined {
  return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
}

/** Return v if it is a string within maxLen; otherwise undefined. */
function safeBoundedString(v: unknown, maxLen: number): string | undefined {
  if (typeof v !== 'string') return undefined;
  return v.length <= maxLen ? v : undefined;
}

/** Cast v to a Record if it is a non-null, non-array object; otherwise null. */
function asObjectOrNull(v: unknown): Record<string, unknown> | null {
  if (v === null || typeof v !== 'object' || Array.isArray(v)) return null;
  return v as Record<string, unknown>;
}

/** Parse the pr field. Returns undefined unless both number (finite) and url (bounded) are valid. */
function parsePr(
  v: unknown,
): { number: number; url: string; reviewState?: string } | undefined {
  const obj = asObjectOrNull(v);
  if (obj === null) return undefined;

  const num = safeFiniteNumber(obj['number']);
  if (num === undefined) return undefined;

  const url = safeBoundedString(obj['url'], URL_MAX_LEN);
  if (url === undefined) return undefined;

  const reviewState = safeBoundedString(obj['reviewState'], REVIEW_STATE_MAX_LEN);
  return {
    number: num,
    url,
    ...(reviewState !== undefined && { reviewState }),
  };
}

/** Parse rate_limits → rate. Returns undefined if rate_limits is not an object.
 *  Returns a (possibly empty) object with undefined sub-fields when the object
 *  exists but sub-field values fail Number.isFinite validation. */
function parseRate(v: unknown): { fiveHourPct?: number; sevenDayPct?: number } | undefined {
  const obj = asObjectOrNull(v);
  if (obj === null) return undefined;

  const fiveHourPct = safeFiniteNumber(obj['fiveHourPct']);
  const sevenDayPct = safeFiniteNumber(obj['sevenDayPct']);

  return {
    ...(fiveHourPct !== undefined && { fiveHourPct }),
    ...(sevenDayPct !== undefined && { sevenDayPct }),
  };
}
