/**
 * Cross-check our Claude Code parsing + pricing against ccusage
 * (independent implementation over the same JSONL files).
 * Run: npm run validate
 */
import { execSync } from 'node:child_process';
import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { parseClaudeSession } from '@neuralmindlabs/tokn';
import { parseCodexSession } from '@neuralmindlabs/tokn';
import { eventsToDays } from '@neuralmindlabs/tokn';
import type { UsageEvent } from '@neuralmindlabs/tokn';

function* jsonlFiles(dir: string): Generator<string> {
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
    const p = join(dir, entry.name);
    if (entry.isDirectory()) yield* jsonlFiles(p);
    else if (entry.name.endsWith('.jsonl')) yield p;
  }
}

const dedupe = new Set<string>();
const events: UsageEvent[] = [];
let files = 0;
for (const file of jsonlFiles(join(homedir(), '.claude', 'projects'))) {
  files++;
  try {
    events.push(...parseClaudeSession(readFileSync(file, 'utf8'), dedupe).events);
  } catch {
    /* unreadable file — skip */
  }
}

// Codex self-check: our per-event deltas must sum to each session's own
// cumulative counter. (ccusage double-counts Codex — token_count events are
// emitted twice — so it can't serve as the reference here.)
let codexMismatches = 0;
let codexEvents = 0;
for (const file of jsonlFiles(join(homedir(), '.codex', 'sessions'))) {
  files++;
  let content: string;
  try {
    content = readFileSync(file, 'utf8');
  } catch {
    continue;
  }
  const parsed = parseCodexSession(content).events;
  codexEvents += parsed.length;
  let finalTotal = 0;
  let resets = 0;
  let prevTotal = -1;
  for (const line of content.split('\n')) {
    if (!line.includes('token_count')) continue;
    try {
      const t = JSON.parse(line).payload?.info?.total_token_usage;
      if (!t) continue;
      if (t.total_tokens < prevTotal) resets++;
      prevTotal = t.total_tokens;
      finalTotal = t.total_tokens;
    } catch {
      /* skip */
    }
  }
  const ourTotal = parsed.reduce((s, e) => s + e.inputTokens + e.outputTokens, 0);
  // with counter resets the session total is unrecoverable from the last value alone
  if (resets === 0 && finalTotal > 0 && ourTotal !== finalTotal) {
    codexMismatches++;
    console.error(`codex mismatch ${file}: ours ${ourTotal} vs counter ${finalTotal}`);
  }
}
console.log(`codex: ${codexEvents} events across sessions, ${codexMismatches} counter mismatches`);
console.log(`parsed ${files} files, ${events.length} claude usage events`);

const days = eventsToDays(events);
const ours = new Map<string, number>();
for (const [date, byModel] of Object.entries(days)) {
  let cost = 0;
  for (const t of Object.values(byModel)) cost += t.costUSD;
  ours.set(date, cost);
}

console.log('running ccusage (this downloads the package on first run)...');
const raw = execSync('npx --yes ccusage@latest daily --json', {
  encoding: 'utf8',
  maxBuffer: 64 * 1024 * 1024,
});
const theirs = JSON.parse(raw);

const unpriced = new Map<string, number>();
for (const byModel of Object.values(days)) {
  for (const [model, t] of Object.entries(byModel)) {
    if (t.unpricedEvents > 0) unpriced.set(model, (unpriced.get(model) ?? 0) + t.unpricedEvents);
  }
}
if (unpriced.size > 0) console.log('models missing pricing:', Object.fromEntries(unpriced));

let worst = 0;
let compared = 0;
const rows: string[] = [];
for (const d of theirs.daily ?? []) {
  const ourCost = ours.get(d.period ?? d.date) ?? 0;
  // only Claude Code is comparable: ccusage double-counts codex, and we
  // don't parse its other agents (gemini, openclaw)
  const theirCost = (d.modelBreakdowns ?? [])
    .filter((mb: any) => typeof mb.modelName === 'string' && mb.modelName.startsWith('claude-'))
    .reduce((s: number, mb: any) => s + (mb.cost ?? 0), 0);
  if (theirCost < 0.01 && ourCost < 0.01) continue;
  compared++;
  const diff = theirCost === 0 ? (ourCost === 0 ? 0 : 1) : Math.abs(ourCost - theirCost) / theirCost;
  worst = Math.max(worst, diff);
  rows.push(
    `${d.period ?? d.date}  ours $${ourCost.toFixed(4).padStart(10)}  ccusage $${theirCost.toFixed(4).padStart(10)}  diff ${(diff * 100).toFixed(2)}%`,
  );
}
console.log(rows.slice(-15).join('\n'));
console.log(`\ncompared ${compared} days, worst relative diff ${(worst * 100).toFixed(2)}%`);
if (worst > 0.02) {
  console.error('FAIL: divergence above 2% tolerance');
  process.exit(1);
}
console.log('PASS: within 2% of ccusage');
