#!/usr/bin/env node
// task-register-gate — Stop hook, two blocks:
//   1. an auto-captured prompt from this session is still untouched (pending);
//   2. the turn did substantive tool work but never registered/updated a task.
// Conversational turns with nothing captured pass. The transcript scan unions
// <id>.jsonl with its <id>.jsonl.live.jsonl sibling: the two lag each other and
// a registration present in either one is a real registration.
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { gateDisabled, readTailText, readTasks, tasksDir, transcriptSources } from './lib/task-store.mjs';

const CLI = path.join(path.dirname(fileURLToPath(import.meta.url)), 'todo-tasks.mjs');

const MUTATING = new Set(['Edit', 'Write', 'NotebookEdit', 'Agent', 'Workflow']);
const TASK_TOOLS = new Set(['TaskCreate', 'TaskUpdate']);
const WORK_TOOLS = new Set([
  'Bash',
  'mcp__plugin_quietcontext_quietcontext__execute',
  'mcp__plugin_quietcontext_quietcontext__batch',
  'mcp__plugin_quietcontext_quietcontext__exec-file',
]);
const WORK_THRESHOLD = 4;
const CLI_REGISTER = /todo-tasks(\.mjs)?\s+(--session\s+\S+\s+)?(new|start|done|dismiss)\b/;
const TAIL_STEPS = [4 << 20, 32 << 20, Infinity];

function block(reason) {
  console.log(JSON.stringify({ decision: 'block', reason: `task-register-gate: ${reason}` }));
}

function typedPromptText(o) {
  if (o?.type !== 'user' || o.isMeta || o.toolUseResult) return null;
  const c = o.message?.content;
  if (typeof c === 'string') return c;
  if (Array.isArray(c)) {
    const texts = c
      .filter((b) => b?.type === 'text' && typeof b.text === 'string')
      .map((b) => b.text.trim())
      .filter((t) => t && t !== '[Request interrupted by user]' && t !== '[Request interrupted by user for tool use]');
    if (texts.length) return texts.join('\n');
  }
  return null;
}

function scanText(text) {
  const rows = [];
  let lastPrompt = -1;
  let parsed = 0;
  for (const line of text.split('\n')) {
    if (!line) continue;
    let o;
    try {
      o = JSON.parse(line);
    } catch {
      continue;
    }
    parsed++;
    rows.push(o);
    if (typedPromptText(o) !== null) lastPrompt = rows.length - 1;
  }
  if (!parsed || lastPrompt < 0) return null;
  let taskTouched = false;
  let mutating = null;
  let workCalls = 0;
  for (let i = lastPrompt + 1; i < rows.length; i++) {
    const o = rows[i];
    if (o?.type !== 'assistant' || !Array.isArray(o.message?.content)) continue;
    for (const b of o.message.content) {
      if (b?.type !== 'tool_use') continue;
      if (TASK_TOOLS.has(b.name)) taskTouched = true;
      if (b.name === 'Bash' && CLI_REGISTER.test(String(b.input?.command || ''))) taskTouched = true;
      if (MUTATING.has(b.name)) mutating = mutating || b.name;
      if (WORK_TOOLS.has(b.name)) workCalls++;
    }
  }
  return { taskTouched, mutating, workCalls };
}

function scanSource(file) {
  let size = 0;
  try {
    size = fs.statSync(file).size;
  } catch {
    return null;
  }
  for (const bytes of TAIL_STEPS) {
    let text;
    try {
      text = bytes >= size ? fs.readFileSync(file, 'utf8') : readTailText(file, bytes);
    } catch {
      return null;
    }
    const r = scanText(text);
    if (r) return r;
    if (bytes >= size) return null;
  }
  return null;
}

function untouchedCaptured(sessionId) {
  if (!sessionId) return [];
  const tasks = readTasks(tasksDir(sessionId));
  if (!tasks) return [];
  return tasks.filter((t) => t?.metadata?.autoCaptured === 'true' && t.status === 'pending');
}

function main() {
  if (gateDisabled()) return;
  let inp = {};
  try {
    inp = JSON.parse(fs.readFileSync(0, 'utf8'));
  } catch {
    block('cannot verify task registration because the hook input is invalid. Register the task and retry.');
    return;
  }

  const stale = untouchedCaptured(inp.session_id);
  if (stale.length) {
    const list = stale.slice(0, 8).map((t) => `#${t.id} ${t.subject.slice(0, 80)}`).join('; ');
    block(
      `${stale.length} auto-captured request(s) from this session are still untouched — ${list}. ` +
        `Each one must end as in_progress (working it), completed (done), or dismissed with a reason ` +
        `(bun ${CLI} dismiss <N> "<reason>") before you stop.`
    );
    return;
  }

  const sources = transcriptSources(inp.transcript_path);
  if (!sources.length) {
    block('cannot verify task registration because the transcript is unavailable. Register the task and retry.');
    return;
  }
  const results = sources.map(scanSource).filter(Boolean);
  if (!results.length) {
    block('cannot verify task registration because no transcript source has a readable user request. Register the task and retry.');
    return;
  }

  if (results.some((r) => r.taskTouched)) return;
  const mutating = results.map((r) => r.mutating).find(Boolean) || null;
  const workCalls = Math.max(...results.map((r) => r.workCalls));
  if (!mutating && workCalls < WORK_THRESHOLD) return;

  const why = mutating ? `used ${mutating}` : `${workCalls} work tool calls`;
  block(
    `this turn did substantive work (${why}) but registered no task. ` +
      `Register the user's request via TaskCreate (or update the matching task via TaskUpdate) before stopping.`
  );
}

main();
process.exit(0);
