/**
 * v2.6 Phase 1-UX (U.6) — pure mapper for opencode's per-step usage event.
 *
 * opencode's warm server emits `session.next.step.ended` once per completed LLM
 * step (so a multi-tool turn fires it several times). Its `properties` carry the
 * step's token + cost accounting:
 *
 *   {
 *     timestamp: number;
 *     sessionID: string;
 *     finish: string;
 *     cost: number;                                  // USD for this step
 *     tokens: {
 *       input: number; output: number; reasoning: number;
 *       cache: { read: number; write: number };
 *     };
 *     snapshot?: string;
 *   }
 *
 * (Verified against @opencode-ai/sdk@1.15.12 — `EventSessionNextStepEnded` in
 * `dist/v2/gen/types.gen.d.ts`, a member of the `Event` union the SSE loop
 * switches on.)
 *
 * We normalize to the review's target slice `{input, output, cost}` (the
 * provider-agnostic `AgentUsage` shape lands later). cache read/write tokens are
 * folded into `input` so the persisted input count reflects the real context the
 * model billed for; reasoning tokens are folded into `output` since that's what
 * the provider counts them as for generation. This keeps the persisted totals a
 * faithful sum of what opencode reported, without inventing extra columns yet.
 */

/** The `properties` shape of a `session.next.step.ended` event (subset we read). */
export interface StepEndedProps {
  cost: number;
  tokens: {
    input: number;
    output: number;
    reasoning: number;
    cache: { read: number; write: number };
  };
}

/** Normalized per-step usage delta persisted onto the agent_sessions row. */
export interface StepUsage {
  input: number;
  output: number;
  cost: number;
}

/** Coerce a possibly-missing/NaN number to a non-negative finite integer (tokens). */
function n(v: unknown): number {
  const x = typeof v === 'number' ? v : Number(v);
  return Number.isFinite(x) && x > 0 ? Math.round(x) : 0;
}

/** Coerce a possibly-missing/NaN number to a non-negative finite float (cost USD). */
function f(v: unknown): number {
  const x = typeof v === 'number' ? v : Number(v);
  return Number.isFinite(x) && x > 0 ? x : 0;
}

/**
 * Map a `session.next.step.ended` payload → the normalized `{input, output, cost}`
 * delta. Defensive against missing/partial token blocks (the wire is trusted but
 * we never want a NaN to poison the accumulated DB total). `input` folds in cache
 * read+write; `output` folds in reasoning.
 */
export function stepEndedToUsage(props: Partial<StepEndedProps> | undefined): StepUsage {
  const t = props?.tokens;
  const cacheRead = n(t?.cache?.read);
  const cacheWrite = n(t?.cache?.write);
  return {
    input: n(t?.input) + cacheRead + cacheWrite,
    output: n(t?.output) + n(t?.reasoning),
    cost: f(props?.cost),
  };
}
