#!/usr/bin/env node
/**
 * router-task-id (✅O17) — persisted first-plan-anchor. task-id присваивается при ПЕРВОМ
 * плане задачи, ПЕРСИСТИТСЯ, НЕ меняется через re-issue (новый plan-hash не сбрасывает).
 * Чистое ядро; персист (loadTaskId/saveTaskId) — отдельно, инъектируемый fs,
 * assertSafeSessionId guard (N3-shared, path-injection).
 */
import { assertSafeSessionId } from './action-journal.mjs';

/** Стабильный task-id: существующий → возвращается как есть (персист); иначе якорь из
 *  firstPlanHash (первый план задачи). Нет ни того ни другого → null. */
export function deriveTaskId({ existingTaskId = null, firstPlanHash = '' } = {}) {
  if (typeof existingTaskId === 'string' && existingTaskId.trim()) return existingTaskId;
  const h = String(firstPlanHash || '').trim();
  return h ? `task:${h}` : null;
}

function taskIdPath(runtimeDir, sessionId) {
  assertSafeSessionId(sessionId);
  const sep = runtimeDir.endsWith('/') ? '' : '/';
  return `${runtimeDir}${sep}router-task-id-${sessionId}.txt`;
}

/** Загрузить персистнутый task-id (или null). fsImpl инъектируем. */
export function loadTaskId({ sessionId, runtimeDir, fsImpl }) {
  try { return String(fsImpl.readFileSync(taskIdPath(runtimeDir, sessionId), 'utf8')).trim() || null; }
  catch (e) { if (e && e.code === 'ENOENT') return null; throw e; }
}

/** Персистнуть task-id (идемпотентно — A0 пишет один раз при первом плане).
 *  F-C1 (sharp-edges): атомарно temp→rename (зеркало plan-lock writeAtomic) — частичная
 *  запись не оставляет битый якорь на финальном пути. */
export function saveTaskId({ taskId, sessionId, runtimeDir, fsImpl }) {
  const path = taskIdPath(runtimeDir, sessionId);
  const tmp = `${path}.tmp`;
  fsImpl.writeFileSync(tmp, String(taskId || ''));
  fsImpl.renameSync(tmp, path);
}
