// task-store — shared reader/writer for the Claude Code task store used by the
// TODO-enforcement hooks (auto-capture, edit gate, stop gate) and todo-tasks CLI.
// Store contract (claude 2.1.226): ~/.claude/tasks/<sanitized-list-id>/<id>.json,
// status enum pending|in_progress|completed, optional metadata record, next id =
// max(numeric file names, .highwatermark) + 1. Listing is uncached, so a file
// written here is visible to the running session's Task tools immediately.
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';

const HOME = process.env.HOME || '/home/user';
export const GATE_OFF = path.join(HOME, '.claude', 'todo-gate-off');

export function gateDisabled() {
  try {
    return fs.existsSync(GATE_OFF);
  } catch {
    return false;
  }
}

export function tasksDir(sessionId) {
  const id = String(sessionId || '').replace(/[^a-zA-Z0-9_-]/g, '-');
  return path.join(HOME, '.claude', 'tasks', id);
}

export function readTasks(dir) {
  let names;
  try {
    names = fs.readdirSync(dir);
  } catch {
    return null;
  }
  const out = [];
  for (const n of names) {
    if (!n.endsWith('.json') || n.startsWith('.')) continue;
    try {
      const t = JSON.parse(fs.readFileSync(path.join(dir, n), 'utf8'));
      if (t && typeof t === 'object') out.push(t);
    } catch {
      /* a partially written task file is not a reason to gate */
    }
  }
  return out.sort((a, b) => Number(a.id) - Number(b.id));
}

export function taskPath(dir, id) {
  return path.join(dir, `${String(id).replace(/[^a-zA-Z0-9_-]/g, '-')}.json`);
}

export function readTask(dir, id) {
  try {
    return JSON.parse(fs.readFileSync(taskPath(dir, id), 'utf8'));
  } catch {
    return null;
  }
}

export function writeTask(dir, task) {
  const p = taskPath(dir, task.id);
  const tmp = `${p}.tmp-${process.pid}`;
  fs.writeFileSync(tmp, `${JSON.stringify(task, null, 2)}\n`);
  fs.renameSync(tmp, p);
}

function highwatermark(dir) {
  try {
    const n = parseInt(fs.readFileSync(path.join(dir, '.highwatermark'), 'utf8').trim(), 10);
    return Number.isNaN(n) ? 0 : n;
  } catch {
    return 0;
  }
}

export function createTask(dir, { subject, description, activeForm, metadata }) {
  fs.mkdirSync(dir, { recursive: true });
  let base = highwatermark(dir);
  for (const n of fs.readdirSync(dir)) {
    if (!n.endsWith('.json') || n.startsWith('.')) continue;
    const v = parseInt(n.slice(0, -5), 10);
    if (!Number.isNaN(v) && v > base) base = v;
  }
  for (let id = base + 1; id <= base + 500; id++) {
    const p = path.join(dir, `${id}.json`);
    let fd;
    try {
      fd = fs.openSync(p, 'wx');
    } catch {
      continue;
    }
    const task = {
      id: String(id),
      subject,
      description,
      activeForm,
      status: 'pending',
      blocks: [],
      blockedBy: [],
      metadata,
    };
    try {
      fs.writeSync(fd, `${JSON.stringify(task, null, 2)}\n`);
    } finally {
      fs.closeSync(fd);
    }
    return task;
  }
  throw new Error('task id allocation exhausted');
}

export function ledgerPath(dir) {
  return path.join(dir, '.autocapture.jsonl');
}

export function readLedger(dir) {
  let raw;
  try {
    raw = fs.readFileSync(ledgerPath(dir), 'utf8');
  } catch {
    return [];
  }
  const out = [];
  for (const line of raw.split('\n')) {
    if (!line.trim()) continue;
    try {
      out.push(JSON.parse(line));
    } catch {
      /* skip */
    }
  }
  return out;
}

export function appendLedger(dir, entry) {
  fs.mkdirSync(dir, { recursive: true });
  fs.appendFileSync(ledgerPath(dir), `${JSON.stringify(entry)}\n`);
}

export function fingerprint(text) {
  return crypto.createHash('sha256').update(String(text)).digest('hex').slice(0, 16);
}

// Closed list of pure-acknowledgement tokens. A prompt is a REQUEST unless every
// one of its word tokens is in this list. Fail-closed: unknown word => request.
const ACK = new Set([
  'a','ack','ah','alright','awesome','beautiful','bye','brb','cheers','cool','damn','done',
  'excellent','exactly','fine','good','goodnight','got','great','ha','haha','hah','hello','hey',
  'hi','hm','hmm','i','indeed','it','k','kk','lmao','lol','love','me','morning','my','nah',
  'never','nice','night','no','nope','np','oh','ok','okay','oops','perfect','please','right',
  'see','sorry','sure','sweet','thank','thanks','thx','ty','ugh','welcome','word','wow','yay',
  'yeah','yep','yes','you','yup',
]);
const MAX_ACK_TOKENS = 6;

// true => create a task for this prompt.
export function isRequest(text) {
  const tokens = String(text || '')
    .toLowerCase()
    .replace(/[^a-z0-9']+/g, ' ')
    .trim()
    .split(/\s+/)
    .filter(Boolean);
  if (!tokens.length) return false;
  if (tokens.length > MAX_ACK_TOKENS) return true;
  return !tokens.every((w) => ACK.has(w));
}

// The harness writes two transcript files that lag each other: <id>.jsonl and
// <id>.jsonl.live.jsonl. Either may hold a tool call the other does not.
export function transcriptSources(tp) {
  if (!tp) return [];
  const cands = [tp, `${tp}.live.jsonl`];
  if (tp.endsWith('.live.jsonl')) cands.push(tp.slice(0, -'.live.jsonl'.length));
  const seen = new Set();
  return cands.filter((p) => {
    if (seen.has(p) || !fs.existsSync(p)) return false;
    seen.add(p);
    return true;
  });
}

// A subagent shares its parent's session id but writes its own transcript, whose
// first record carries isSidechain. Unknown/unreadable counts as main thread.
export function isSidechain(transcriptPath) {
  if (!transcriptPath) return false;
  if (path.basename(transcriptPath).startsWith('agent-')) return true;
  for (const p of transcriptSources(transcriptPath)) {
    try {
      const head = readHeadText(p, 1 << 16).split('\n')[0];
      if (JSON.parse(head)?.isSidechain === true) return true;
    } catch {
      /* next source */
    }
  }
  return false;
}

function readHeadText(file, bytes) {
  const fd = fs.openSync(file, 'r');
  try {
    const len = Math.min(bytes, fs.fstatSync(fd).size);
    const buf = Buffer.allocUnsafe(len);
    fs.readSync(fd, buf, 0, len, 0);
    return buf.toString('utf8');
  } finally {
    fs.closeSync(fd);
  }
}

export function readTailText(file, bytes) {
  const fd = fs.openSync(file, 'r');
  try {
    const size = fs.fstatSync(fd).size;
    const start = bytes >= size ? 0 : size - bytes;
    const len = size - start;
    const buf = Buffer.allocUnsafe(len);
    fs.readSync(fd, buf, 0, len, start);
    const s = buf.toString('utf8');
    if (start === 0) return s;
    const i = s.indexOf('\n');
    return i < 0 ? '' : s.slice(i + 1);
  } finally {
    fs.closeSync(fd);
  }
}
