#!/usr/bin/env node

// NERF Context Monitor Hook (PostToolUse)
// Adapted from pi's context monitor pattern for NERF engagements.
//
// Reads context metrics from the bridge file written by nerf-statusline.js
// and injects warnings as additionalContext when remaining context drops
// below thresholds.
//
// Thresholds:
//   WARNING  — remaining <= 35%
//   CRITICAL — remaining <= 25%
//
// NERF-specific: if .nerf/ENGAGEMENT.md exists in cwd, warnings are
// tailored to prompt saving engagement state before context runs out.

"use strict";

const fs = require("fs");
const path = require("path");

// --- Constants ---

const STDIN_TIMEOUT_MS = 3000;

// Context thresholds (percent remaining)
const THRESHOLD_WARNING = 35;
const THRESHOLD_CRITICAL = 25;

// Debounce: minimum tool uses between repeated warnings at the same severity
const DEBOUNCE_INTERVAL = 5;

// Bridge file metrics older than this are considered stale and ignored
const STALE_THRESHOLD_MS = 60_000;

// State file for debounce tracking (per-session, written alongside bridge file)
const STATE_PREFIX = "/tmp/nerf-ctx-monitor-state-";

// --- Helpers ---

function readStdinWithTimeout(timeoutMs) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      process.stdin.destroy();
      reject(new Error("stdin timeout"));
    }, timeoutMs);

    const chunks = [];
    process.stdin.setEncoding("utf8");
    process.stdin.on("data", (chunk) => chunks.push(chunk));
    process.stdin.on("end", () => {
      clearTimeout(timer);
      resolve(chunks.join(""));
    });
    process.stdin.on("error", (err) => {
      clearTimeout(timer);
      reject(err);
    });
    process.stdin.resume();
  });
}

/**
 * Determine the current severity level from remaining context percentage.
 * Returns null if above all thresholds.
 */
function getSeverity(percentRemaining) {
  if (percentRemaining <= THRESHOLD_CRITICAL) return "CRITICAL";
  if (percentRemaining <= THRESHOLD_WARNING) return "WARNING";
  return null;
}

/**
 * Load debounce state from the per-session state file.
 * Returns { lastSeverity, toolUsesSinceWarning } or defaults.
 */
function loadState(sessionId) {
  const defaults = { lastSeverity: null, toolUsesSinceWarning: 0 };
  if (!sessionId) return defaults;

  try {
    const raw = fs.readFileSync(`${STATE_PREFIX}${sessionId}.json`, "utf8");
    return { ...defaults, ...JSON.parse(raw) };
  } catch {
    return defaults;
  }
}

/**
 * Persist debounce state.
 */
function saveState(sessionId, state) {
  if (!sessionId) return;
  try {
    fs.writeFileSync(
      `${STATE_PREFIX}${sessionId}.json`,
      JSON.stringify(state),
      { mode: 0o600 }
    );
  } catch {
    // Silent fail
  }
}

/**
 * Check if a NERF engagement is active by looking for .nerf/ENGAGEMENT.md
 * in the current working directory.
 */
function hasEngagementContext(cwd) {
  try {
    return fs.existsSync(path.join(cwd, ".nerf", "ENGAGEMENT.md"));
  } catch {
    return false;
  }
}

/**
 * Build the warning message. NERF engagements get tailored advice
 * about saving engagement state.
 */
function buildWarning(severity, percentRemaining, isEngagement) {
  const pct = percentRemaining.toFixed(0);

  if (severity === "CRITICAL") {
    if (isEngagement) {
      return (
        `CONTEXT CRITICAL (${pct}% remaining): Context nearly exhausted. ` +
        `Save engagement state to .nerf/ENGAGEMENT.md immediately ` +
        `(mission, phase, findings, open questions, next actions), ` +
        `then inform the user that a new session is needed to continue.`
      );
    }
    return (
      `CONTEXT CRITICAL (${pct}% remaining): Context nearly exhausted. ` +
      `Summarize findings and inform the user that context is running out. ` +
      `Recommend starting a new session for further work.`
    );
  }

  // WARNING
  if (isEngagement) {
    return (
      `CONTEXT WARNING (${pct}% remaining): Context getting limited. ` +
      `Wrap up current analysis phase. Consider saving engagement state ` +
      `to .nerf/ENGAGEMENT.md so work can resume in a fresh session.`
    );
  }
  return (
    `CONTEXT WARNING (${pct}% remaining): Context getting limited. ` +
    `Wrap up current analysis and provide findings summary. ` +
    `Avoid opening new investigation threads.`
  );
}

// --- Main ---

async function main() {
  // Read tool event data from stdin
  let raw;
  try {
    raw = await readStdinWithTimeout(STDIN_TIMEOUT_MS);
  } catch {
    process.exit(0);
  }

  let event;
  try {
    event = JSON.parse(raw);
  } catch {
    process.exit(0);
  }

  const sessionId = event.session_id || null;
  const cwd = event.cwd || process.cwd();

  // Read bridge file written by nerf-statusline.js
  if (!sessionId) process.exit(0);

  const bridgePath = `/tmp/nerf-ctx-${sessionId}.json`;
  let metrics;
  try {
    const bridgeRaw = fs.readFileSync(bridgePath, "utf8");
    metrics = JSON.parse(bridgeRaw);
  } catch {
    // No bridge file yet — statusline hook hasn't run. Exit silently.
    process.exit(0);
  }

  // Ignore stale metrics (older than 60 seconds)
  const age = Date.now() - (metrics.timestamp || 0);
  if (age > STALE_THRESHOLD_MS) {
    process.exit(0);
  }

  const percentRemaining = metrics.percent_remaining;
  const severity = getSeverity(percentRemaining);

  // No threshold breached — nothing to do
  if (!severity) {
    // Reset debounce counter since we're back in the clear
    saveState(sessionId, { lastSeverity: null, toolUsesSinceWarning: 0 });
    process.exit(0);
  }

  // Debounce logic: don't spam warnings on every tool use
  const state = loadState(sessionId);
  state.toolUsesSinceWarning++;

  // Severity escalation (WARNING -> CRITICAL) bypasses debounce
  const isEscalation =
    severity === "CRITICAL" && state.lastSeverity === "WARNING";

  if (!isEscalation && state.lastSeverity === severity) {
    if (state.toolUsesSinceWarning < DEBOUNCE_INTERVAL) {
      // Not enough tool uses since last warning at this severity — stay quiet
      saveState(sessionId, state);
      process.exit(0);
    }
  }

  // Emit the warning
  const isEngagement = hasEngagementContext(cwd);
  const warning = buildWarning(severity, percentRemaining, isEngagement);

  const output = {
    hookSpecificOutput: {
      hookEventName: "PostToolUse",
      additionalContext: warning,
    },
  };

  process.stdout.write(JSON.stringify(output) + "\n");

  // Update debounce state
  saveState(sessionId, {
    lastSeverity: severity,
    toolUsesSinceWarning: 0,
  });
}

main().catch(() => process.exit(0));
