/**
 * SET 14 — Tier 1 metrics display helpers (web).
 * Keep in lockstep with src/tier1/metricsHelpers.ts (tested there).
 */

/** Format elapsed seconds as H:MM:SS (hours unpadded, minutes/seconds zero-padded). */
export function formatHms(totalSeconds: number): string {
  const s = Math.max(0, Math.floor(Number(totalSeconds) || 0));
  const h = Math.floor(s / 3600);
  const m = Math.floor((s % 3600) / 60);
  const sec = s % 60;
  return `${h}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
}

/** Σ passesPerFeature — the only authoritative cycle total (SET 14a). */
export function derivePassesTotal(passesPerFeature?: Record<string, number> | null): number {
  if (!passesPerFeature || typeof passesPerFeature !== 'object') return 0;
  let sum = 0;
  for (const n of Object.values(passesPerFeature)) sum += Number(n) || 0;
  return sum;
}

/**
 * Normalize substrate "0 tokens" phrasing so the log always shows `(0 tok)`.
 * LLM call annotations from offscreen already use the `(N→ / M← tok)` form.
 */
export function displayPipelineMessage(message: string): string {
  if (/\(\s*0\s*tok\s*\)/i.test(message)) return message;
  if (/\b0\s*tokens?\b/i.test(message)) {
    return message.replace(/\b0\s*tokens?\b/i, '(0 tok)');
  }
  return message;
}

/** Cycles for the card: prefer Σ ppf, else passesTotal. */
export function cyclesFromMetrics(m: {
  passesTotal?: number;
  passesPerFeature?: Record<string, number>;
} | null | undefined): number {
  if (!m) return 0;
  if (m.passesPerFeature && Object.keys(m.passesPerFeature).length > 0) {
    return derivePassesTotal(m.passesPerFeature);
  }
  return Number(m.passesTotal) || 0;
}

// ── SET 29b — tab-stub indicator: prefix the tab title while a run is live ──

export const BASE_TAB_TITLE = 'RHA Wrapper';

/** "⟳ 12m — RHA Wrapper" — minute ticks; the title prefix is load-bearing. */
export function formatRunningTabTitle(minutes: number, baseTitle: string = BASE_TAB_TITLE): string {
  const m = Math.max(0, Math.floor(Number(minutes) || 0));
  return `⟳ ${m}m — ${baseTitle}`;
}

/** Elapsed whole minutes since an ISO start timestamp, floored at 0. */
export function elapsedMinutesSince(startIso: string, nowMs: number = Date.now()): number {
  const startMs = Date.parse(startIso);
  if (!Number.isFinite(startMs)) return 0;
  return Math.max(0, Math.floor((nowMs - startMs) / 60000));
}

// ── SET 29b — favicon spinner frames (enhancement; title prefix is the one
// that's load-bearing). Inline SVG data URIs — no extra asset files, no
// network fetch, cheap to swap on an interval while a run is live.

function spinnerFaviconFrame(startDeg: number): string {
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">`
    + `<circle cx="12" cy="12" r="9" fill="none" stroke="#2b2440" stroke-width="3"/>`
    + `<path d="M12 3a9 9 0 0 1 9 9" fill="none" stroke="#b14dff" stroke-width="3" `
    + `stroke-linecap="round" transform="rotate(${startDeg} 12 12)"/>`
    + `</svg>`;
  return `data:image/svg+xml,${encodeURIComponent(svg)}`;
}

/** 4 rotated-arc frames — swapped on an interval while `t1?.running` is true. */
export const SPIN_FAVICON_FRAMES: readonly string[] = [0, 90, 180, 270].map(spinnerFaviconFrame);
