#!/usr/bin/env node
/**
 * PreToolUse(AskUserQuestion) -- cosmetic-AskUser hard-block detector (router-gate v4.1).
 *
 * Catches the pattern: simple A/B AskUser used as a substitute for structured
 * ideation (brainstorming/writing-plans). Per-turn -> soft flag; >2/session
 * without brainstorming skill -> hard-block.
 *
 * Spec: docs/superpowers/specs/2026-05-29-router-gate-v4-1-max-closure.md §4.5
 *
 * decide() is pure. main() wires session/turn state from sentinels + transcript.
 */
import {
  readStdin,
  parseEventJson,
  readTranscript,
  sessionToolUses,
  turnToolUses,
  runtimeDir,
  appendRationalizationFlag,
  exitDecision,
} from './enforce-hook-helpers.mjs';
import { existsSync, readFileSync, appendFileSync } from 'node:fs';
import { join } from 'node:path';

/** True if the AskUser is a "simple A/B" (2 short options, no skill mention). */
export function isSimpleAB(questions) {
  if (!Array.isArray(questions) || questions.length === 0) return false;
  return questions.every((q) =>
    q && Array.isArray(q.options) &&
    q.options.length === 2 &&
    q.options.every((o) => o && typeof o.label === 'string' && o.label.length < 30) &&
    !q.options.some((o) => o && typeof o.label === 'string' && o.label.toLowerCase().includes('skill')),
  );
}

// Calibration 5 (2026-05-31) — git-operation APPROVAL prompts are the sanctioned
// git-approval channel (enforce-askuser-answer-parser turns the chosen answer
// into an approve_git_operation record), never a substitute for structured
// ideation. They must NOT be treated as cosmetic A/B. Identified structurally:
// an option label is a literal git command. (SCOPE fix, not a discipline drop —
// see decide(): design A/B questions with non-git labels are unaffected.)
const GIT_CMD_RE = /\bgit\s+(?:commit|push|add|pull|merge|rebase|reset|checkout|switch|branch|stash|cherry-pick|revert|clean|restore|fetch|tag)\b/i;

/** True if this AskUser is a git-operation approval prompt (an option label is a git command). */
export function isGitApprovalQuestion(questions) {
  if (!Array.isArray(questions)) return false;
  return questions.some((q) =>
    q && Array.isArray(q.options) &&
    q.options.some((o) => o && typeof o.label === 'string' && GIT_CMD_RE.test(o.label)));
}

/**
 * Pure cosmetic-AskUser decision (v4.1 §4.5).
 * Caller passes PRIOR counts; decide computes prospective new counts.
 * Hard-block (session >2 simple w/o brainstorming) takes precedence over per-turn soft_flag.
 *
 * @returns {{action:'allow'|'soft_flag'|'hard_block', block:boolean, reason:string|null, isSimpleAB:boolean, newSessionCount:number, newTurnCount:number}}
 */
export function decide({ questions, simpleCountSession = 0, simpleCountTurn = 0, skillMatchedThisTurn = false, brainstormingInvoked = false }) {
  // Calibration 5: git-operation approval prompts are exempt — the sanctioned
  // git-approval channel, never cosmetic ideation. Allow, do not count, never
  // block. (Cannot be abused to dodge ideation discipline: a git-command label
  // makes the answer a real approve_git_operation, not a cosmetic clarification.)
  if (isGitApprovalQuestion(questions)) {
    return { action: 'allow', block: false, reason: null, isSimpleAB: false, newSessionCount: simpleCountSession, newTurnCount: simpleCountTurn };
  }
  const simple = isSimpleAB(questions);
  const newSessionCount = simpleCountSession + (simple ? 1 : 0);
  const newTurnCount = simpleCountTurn + (simple ? 1 : 0);

  if (!simple) {
    return { action: 'allow', block: false, reason: null, isSimpleAB: false, newSessionCount, newTurnCount };
  }

  // Per-session hard-block first (precedence).
  if (newSessionCount > 2 && !brainstormingInvoked) {
    return {
      action: 'hard_block',
      block: true,
      reason: 'v4.1 cosmetic AskUser hard-block: >2 simple AskUser in session without brainstorming skill. ' +
        'This is a cosmetic clarification pattern instead of structured ideation. Invoke superpowers:brainstorming now.',
      isSimpleAB: true,
      newSessionCount,
      newTurnCount,
    };
  }

  // Per-turn soft flag.
  if (newTurnCount >= 1 && !skillMatchedThisTurn) {
    return {
      action: 'soft_flag',
      block: false,
      reason: 'v4.1 cosmetic AskUser: simple A/B without active Skill match in turn. ' +
        'If clarification -- continue; if this replaces brainstorming/writing-plans skill -- invoke Skill now.',
      isSimpleAB: true,
      newSessionCount,
      newTurnCount,
    };
  }

  return { action: 'allow', block: false, reason: null, isSimpleAB: true, newSessionCount, newTurnCount };
}

/** Count prior simple-AB AskUser entries from the persisted flags array. */
export function countSimpleSession(flags) {
  if (!Array.isArray(flags)) return 0;
  return flags.filter((f) => f && f.isSimpleAB === true).length;
}

/** True if superpowers:brainstorming was invoked anywhere this session. */
export function brainstormingInvokedSession(entries) {
  return sessionToolUses(entries).some((u) =>
    u.name === 'Skill' && typeof u.input?.skill === 'string' && u.input.skill.includes('brainstorming'));
}

/** True if any Skill tool was invoked in the current turn. */
export function skillMatchedThisTurn(entries) {
  return turnToolUses(entries).some((u) => u.name === 'Skill');
}

function flagsPath(sessionId) {
  return join(runtimeDir(), `ask-user-cosmetic-flags-${sessionId || 'unknown'}.jsonl`);
}

function readFlags(sessionId) {
  try {
    const p = flagsPath(sessionId);
    if (!existsSync(p)) return [];
    return readFileSync(p, 'utf-8').split('\n').filter(Boolean).map((l) => {
      try { return JSON.parse(l); } catch { return null; }
    }).filter(Boolean);
  } catch { return []; }
}

export async function main() {
  try {
    const raw = await readStdin();
    const event = parseEventJson(raw);
    if (!event || event.tool_name !== 'AskUserQuestion') return exitDecision({ block: false });

    const questions = event.tool_input?.questions || [];
    const sessionId = event.session_id || 'unknown';
    const transcript = readTranscript(event.transcript_path);

    const priorFlags = readFlags(sessionId);
    const simpleCountSession = countSimpleSession(priorFlags);
    const brainstormingInvoked = brainstormingInvokedSession(transcript);
    const skillThisTurn = skillMatchedThisTurn(transcript);

    const result = decide({
      questions,
      simpleCountSession,
      simpleCountTurn: 0,
      skillMatchedThisTurn: skillThisTurn,
      brainstormingInvoked,
    });

    try {
      appendFileSync(flagsPath(sessionId), JSON.stringify({
        ts: new Date().toISOString(),
        session_id: sessionId,
        isSimpleAB: result.isSimpleAB,
        action: result.action,
        askuser_structure: result.isSimpleAB ? 'simple_ab' : 'multi_option',
      }) + '\n');
    } catch { /* ignore persistence errors */ }

    if (result.action === 'soft_flag') {
      appendRationalizationFlag(sessionId, 'cosmetic_askuser_soft', result.reason);
      return exitDecision({ block: false });
    }
    if (result.action === 'hard_block') {
      appendRationalizationFlag(sessionId, 'cosmetic_askuser_hard', result.reason);
      return exitDecision({ block: true, message: '[askuser-cosmetic-detector] ' + result.reason });
    }
    return exitDecision({ block: false });
  } catch {
    return exitDecision({ block: false }); // fail-open
  }
}

const isCli = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/askuser-cosmetic-detector.mjs');
if (isCli) main();
