#!/usr/bin/env node
// Spike S4 — découverte des sessions JSONL (~/.claude/projects) + registre (~/.claude/sessions)
// Critères Go : ≥95 % des JSONL exploitables (sessionId+cwd), scan < 3 s (tête+queue),
// validation pid/procStart du registre, parseur strictement tolérant.
import { readdirSync, statSync, openSync, readSync, closeSync, readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';

const PROJECTS = join(homedir(), '.claude', 'projects');
const SESSIONS = join(homedir(), '.claude', 'sessions');
const HEAD_BYTES = 256 * 1024;
const TAIL_BYTES = 64 * 1024;

function readChunk(path, position, length) {
  const fd = openSync(path, 'r');
  try {
    const buf = Buffer.alloc(length);
    const n = readSync(fd, buf, 0, length, position);
    return buf.subarray(0, n).toString('utf8');
  } finally { closeSync(fd); }
}

// Parse tolérant : chaque ligne en try/catch, types inconnus ignorés.
function parseLines(text, partialFirst = false, partialLast = false) {
  const lines = text.split('\n');
  if (partialFirst) lines.shift(); // 1re ligne potentiellement coupée (lecture de queue)
  if (partialLast) lines.pop();    // dernière ligne potentiellement coupée (lecture de tête)
  const out = [];
  for (const l of lines) {
    if (!l.trim()) continue;
    try { out.push(JSON.parse(l)); } catch { /* ligne coupée ou malformée : ignorée */ }
  }
  return out;
}

function extractMeta(objs, meta) {
  for (const o of objs) {
    if (o.sessionId && !meta.sessionId) meta.sessionId = o.sessionId;
    if (o.cwd && !meta.cwd) meta.cwd = o.cwd;
    if (o.gitBranch && !meta.gitBranch) meta.gitBranch = o.gitBranch;
    if (o.version && !meta.version) meta.version = o.version;
    if (o.slug && !meta.slug) meta.slug = o.slug;
    if (o.type && meta.types) meta.types.add(o.type);
    if (o.timestamp) meta.lastTs = o.timestamp;
    if (o.isSidechain === true) meta.hasSidechain = true;
  }
}

// ---------- 1. Scan ~/.claude/projects ----------
const t0 = process.hrtime.bigint();
const report = { files: 0, bytes: 0, ok: 0, noCwd: 0, noSessionId: 0, empty: 0, errors: 0 };
const versions = new Map();
const allTypes = new Map(); // type de ligne → occurrences (sur les chunks lus)
const failures = [];
const sessions = [];

for (const dir of readdirSync(PROJECTS)) {
  const dirPath = join(PROJECTS, dir);
  let entries;
  try { entries = readdirSync(dirPath, { withFileTypes: true }); } catch { continue; }
  for (const e of entries) {
    if (!e.isFile() || !e.name.endsWith('.jsonl')) continue;
    const fp = join(dirPath, e.name);
    report.files++;
    try {
      const st = statSync(fp);
      report.bytes += st.size;
      if (st.size === 0) { report.empty++; failures.push({ fp, why: 'vide' }); continue; }
      const meta = { types: new Set() };
      const head = readChunk(fp, 0, Math.min(HEAD_BYTES, st.size));
      extractMeta(parseLines(head, false, st.size > HEAD_BYTES), meta);
      if (st.size > HEAD_BYTES + TAIL_BYTES) {
        const tail = readChunk(fp, st.size - TAIL_BYTES, TAIL_BYTES);
        extractMeta(parseLines(tail, true, false), meta);
      }
      for (const t of meta.types) allTypes.set(t, (allTypes.get(t) ?? 0) + 1);
      if (!meta.sessionId) { report.noSessionId++; failures.push({ fp, why: 'pas de sessionId', types: [...meta.types] }); continue; }
      if (!meta.cwd) { report.noCwd++; failures.push({ fp, why: 'pas de cwd', types: [...meta.types] }); continue; }
      report.ok++;
      if (meta.version) versions.set(meta.version, (versions.get(meta.version) ?? 0) + 1);
      sessions.push({ dir, file: e.name, sessionId: meta.sessionId, cwd: meta.cwd, branch: meta.gitBranch, mtimeMs: st.mtimeMs });
    } catch (err) {
      report.errors++;
      failures.push({ fp, why: `exception: ${err.message}` });
    }
  }
}
const scanMs = Number(process.hrtime.bigint() - t0) / 1e6;

// ---------- 2. Validation du mapping nom-de-dossier vs cwd ----------
const munge = (p) => p.replace(/[^A-Za-z0-9]/g, '-');
let mungeMatch = 0, mungeMismatch = 0;
const mismatchSamples = [];
for (const s of sessions) {
  if (munge(s.cwd) === s.dir) mungeMatch++;
  else { mungeMismatch++; if (mismatchSamples.length < 5) mismatchSamples.push({ dir: s.dir, cwd: s.cwd }); }
}

// ---------- 3. Registre ~/.claude/sessions ----------
const registry = [];
if (existsSync(SESSIONS)) {
  for (const f of readdirSync(SESSIONS)) {
    if (!f.endsWith('.json')) continue;
    try {
      const r = JSON.parse(readFileSync(join(SESSIONS, f), 'utf8'));
      let alive = false, procStartOk = null;
      try {
        const stat = readFileSync(`/proc/${r.pid}/stat`, 'utf8');
        // champ 22 = starttime ; le nom du process (champ 2) peut contenir espaces/parenthèses → parser après ')'
        const after = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
        const starttime = after[19]; // champ 22 global = index 19 après le champ 3
        alive = true;
        procStartOk = String(r.procStart) === String(starttime);
      } catch { alive = false; }
      registry.push({ file: f, pid: r.pid, sessionId: r.sessionId, cwd: r.cwd, status: r.status,
        waitingFor: r.waitingFor ?? null, kind: r.kind, version: r.version, peerProtocol: r.peerProtocol,
        updatedAt: r.updatedAt, alive, procStartOk });
    } catch (err) { registry.push({ file: f, error: err.message }); }
  }
}

// ---------- 4. Rattachement cwd → repos réels ----------
const ROOT = join(homedir(), 'WebstormProjects');
let inRoot = 0, outRoot = 0;
const byTopDir = new Map();
for (const s of sessions) {
  if (s.cwd.startsWith(ROOT + '/') || s.cwd === ROOT) {
    inRoot++;
    const top = s.cwd === ROOT ? '(racine)' : s.cwd.slice(ROOT.length + 1).split('/')[0];
    byTopDir.set(top, (byTopDir.get(top) ?? 0) + 1);
  } else outRoot++;
}

// ---------- Rapport ----------
const pct = (n) => report.files ? ((n / report.files) * 100).toFixed(1) + ' %' : 'n/a';
console.log(JSON.stringify({
  scan: { ...report, mb: (report.bytes / 1048576).toFixed(0), scanMs: scanMs.toFixed(0), okPct: pct(report.ok) },
  mungeValidation: { mungeMatch, mungeMismatch, mismatchSamples },
  lineTypes: Object.fromEntries([...allTypes.entries()].sort((a, b) => b[1] - a[1])),
  versions: Object.fromEntries([...versions.entries()].sort()),
  registry: { count: registry.length, entries: registry },
  attachment: { inWebstormProjects: inRoot, outside: outRoot,
    topDirs: Object.fromEntries([...byTopDir.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15)) },
  failuresSample: failures.slice(0, 10),
}, null, 1));
