#!/usr/bin/env node
/**
 * Router state enricher for observer episodes.
 * Reads ~/.claude/runtime/router-state-<sessionId>.json and exposes pure
 * extraction helpers for primary_rationale enrichment.
 *
 * Pure-ish — fs is parameterized via options.baseDir for testability.
 *
 * Per spec: docs/superpowers/specs/2026-05-24-router-stage3-three-fixes-design.md
 */

import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';

function defaultBaseDir() {
  return join(homedir(), '.claude', 'runtime');
}

export function readRouterState(sessionId, options = {}) {
  if (!sessionId || typeof sessionId !== 'string') return null;
  const baseDir = options.baseDir || defaultBaseDir();
  const path = join(baseDir, `router-state-${sessionId}.json`);
  if (!existsSync(path)) return null;
  try {
    const content = readFileSync(path, 'utf-8');
    return JSON.parse(content);
  } catch {
    return null;
  }
}

export function extractRouterFields(state) {
  if (!state || typeof state !== 'object') {
    return { recommended_node: null, recommended_chain: null, chain_progress: [], chain_completed: false };
  }
  const cls = state.classification || {};
  return {
    recommended_node: (cls.recommendedNode || cls.recommended_node) || null,
    recommended_chain: (cls.recommendedChain || cls.recommended_chain || cls.recommended_chain_id) || null,
    chain_progress: Array.isArray(state.chainProgress) ? state.chainProgress : [],
    chain_completed: state.chainCompleted === true,
  };
}

/**
 * Extract the LLM classifier's output for the v4 episode schema (Task 15).
 * Pulls the subset of classification fields the analyzer / brain-retro skill
 * cares about. Returns null when the state has no classification (degraded
 * path, parser running on a transcript with no prehook state).
 */
export function extractClassifierOutput(state) {
  const cls = state?.classification;
  if (!cls || typeof cls !== 'object') return null;
  return {
    task_type: cls.task_type ?? cls.taskType ?? null,
    recommended_node: cls.recommended_node ?? cls.recommendedNode ?? null,
    recommended_chain: cls.recommended_chain ?? cls.recommendedChain ?? null,
    recommended_chain_id: cls.recommended_chain_id ?? null,
    no_skill_found: cls.no_skill_found === true,
    source: cls.source ?? null,
    // Factor-analysis signal: classifier's stated rationale + confidence.
    // Field name varies by prompt schema: new (Phase 2) uses `reason_for_choice`,
    // legacy uses `reasoning`. Null on regex / prefilter paths. Truncated to
    // keep episode JSONL line size bounded.
    reasoning: pickReasoning(cls),
    confidence: typeof cls.confidence === 'number' ? cls.confidence : null,
    // Pass 2 metrics (project-brain-factor-analysis-4passes): network latency,
    // internal retry count, categorized transport error, and the classifier's
    // own top-3 alternative nodes with rejection rationale. null on regex /
    // prefilter / cache paths where the LLM was never (or was already) called.
    latency_ms: typeof cls.latency_ms === 'number' ? cls.latency_ms : null,
    retry_count_internal: typeof cls.retry_count_internal === 'number' ? cls.retry_count_internal : null,
    llm_error: cls.llm_error_type ?? null,
    alternatives_considered: pickAlternatives(cls),
  };
}

function pickReasoning(cls) {
  const v = cls.reasoning ?? cls.reason_for_choice ?? cls.reason ?? null;
  if (typeof v !== 'string') return null;
  return v.slice(0, 600);
}

function pickAlternatives(cls) {
  const v = cls.alternatives_considered;
  if (!Array.isArray(v)) return null;
  // Cap at top-3 to bound episode JSONL line size; Sonnet sometimes returns 5+.
  return v.slice(0, 3);
}
