#!/usr/bin/env node

'use strict';

/**
 * Context monitor for @evolvconsulting/evolv-coder-kit
 * Runs as a PostToolUse hook to warn when context window usage approaches capacity.
 * Reads metrics from the bridge file written by the statusline hook,
 * applies threshold/debounce logic, and injects warnings via additionalContext.
 * Non-blocking — exits 0 always, outputs {} on any error.
 */

const fs = require('fs');
const path = require('path');
const os = require('os');
const crypto = require('crypto');

// ─── Constants ──────────────────────────────────────────────────────────────

const PREFS_PATH = path.join(os.homedir(), '.claude', 'evolv-coder-kit', 'user-preferences.json');
const DEFAULT_WARN_PERCENT = 70;
const DEFAULT_CRITICAL_PERCENT = 85;
const DEBOUNCE_COUNT = 5;
const WARMUP_COUNT = 10;
const FRESHNESS_WINDOW_SEC = 60;

// ─── Configuration ──────────────────────────────────────────────────────────

/**
 * Resolve warning thresholds from env vars > user prefs > defaults.
 * @param {object} [options] - Override paths for testing
 * @returns {{ warnPercent: number, criticalPercent: number }}
 */
function resolveThresholds(options = {}) {
  const prefsPath = options.prefsPath || PREFS_PATH;

  let warnPercent = DEFAULT_WARN_PERCENT;
  let criticalPercent = DEFAULT_CRITICAL_PERCENT;

  // Layer 1: user-preferences.json
  try {
    if (fs.existsSync(prefsPath)) {
      const prefs = JSON.parse(fs.readFileSync(prefsPath, 'utf-8'));
      if (prefs.contextMonitor) {
        const pw = parseInt(prefs.contextMonitor.warnPercent, 10);
        const pc = parseInt(prefs.contextMonitor.criticalPercent, 10);
        if (!isNaN(pw)) warnPercent = pw;
        if (!isNaN(pc)) criticalPercent = pc;
      }
    }
  } catch {
    // Ignore prefs read errors — use defaults
  }

  // Layer 2: env vars (highest priority)
  const envWarn = parseInt(process.env.EVOLV_CTX_WARN, 10);
  const envCrit = parseInt(process.env.EVOLV_CTX_CRITICAL, 10);
  if (!isNaN(envWarn)) warnPercent = envWarn;
  if (!isNaN(envCrit)) criticalPercent = envCrit;

  return { warnPercent, criticalPercent };
}

// ─── Session ID ─────────────────────────────────────────────────────────────

/**
 * Resolve session ID from env var or derive from PWD + PPID.
 * @returns {string}
 */
function resolveSessionId() {
  if (process.env.CLAUDE_SESSION_ID) {
    return process.env.CLAUDE_SESSION_ID;
  }
  const input = `${process.cwd()}${process.ppid}`;
  return crypto.createHash('sha256').update(input).digest('hex').slice(0, 8);
}

// ─── Bridge File ────────────────────────────────────────────────────────────

/**
 * Read the bridge file written by the statusline hook.
 * @param {string} sessionId
 * @returns {object|null} Bridge data or null if unavailable/invalid
 */
function readBridgeFile(sessionId) {
  const bridgePath = path.join(os.tmpdir(), `claude-ctx-${sessionId}.json`);
  try {
    if (!fs.existsSync(bridgePath)) return null;
    const raw = fs.readFileSync(bridgePath, 'utf-8');
    const data = JSON.parse(raw);
    if (typeof data !== 'object' || data === null) return null;
    return data;
  } catch {
    return null;
  }
}

// ─── Monitor State ──────────────────────────────────────────────────────────

/**
 * Read monitor state for a session.
 * @param {string} sessionId
 * @returns {{ toolUseCount: number, debounceCounter: number, lastWarnPercent: number }}
 */
function readState(sessionId) {
  const statePath = path.join(os.tmpdir(), `claude-ctx-monitor-${sessionId}.json`);
  try {
    if (fs.existsSync(statePath)) {
      const data = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
      return {
        toolUseCount: data.toolUseCount || 0,
        debounceCounter: data.debounceCounter || 0,
        lastWarnPercent: data.lastWarnPercent || 0,
      };
    }
  } catch {
    // Ignore read errors
  }
  return { toolUseCount: 0, debounceCounter: 0, lastWarnPercent: 0 };
}

/**
 * Write monitor state atomically.
 * @param {string} sessionId
 * @param {object} state
 */
function writeState(sessionId, state) {
  const statePath = path.join(os.tmpdir(), `claude-ctx-monitor-${sessionId}.json`);
  try {
    const tmp = statePath + '.tmp';
    fs.writeFileSync(tmp, JSON.stringify(state), 'utf-8');
    fs.renameSync(tmp, statePath);
  } catch {
    // Ignore write errors
  }
}

// ─── Warning Messages ───────────────────────────────────────────────────────

/**
 * Build a plain-text warning message for context usage.
 * @param {number} contextPercent
 * @param {'warning'|'critical'} severity
 * @returns {string}
 */
function buildWarningMessage(contextPercent, severity) {
  if (severity === 'critical') {
    return `CRITICAL: Context at ${contextPercent}%. Strongly recommend /eck:fresh-context NOW to preserve work. Current context may be compressed soon.`;
  }
  return `Context usage at ${contextPercent}%. Consider wrapping up the current task or using /eck:fresh-context to continue in a new session.`;
}

// ─── Evaluation Logic ───────────────────────────────────────────────────────

/**
 * Evaluate whether a warning should be emitted.
 * @param {object} bridgeData - Data from bridge file
 * @param {object} state - Current monitor state
 * @param {object} thresholds - { warnPercent, criticalPercent }
 * @returns {{ action: 'warn'|'skip', message?: string, severity?: 'warning'|'critical' }}
 */
function evaluate(bridgeData, state, thresholds) {
  const contextPercent = bridgeData.contextPercent;

  // No context data available
  if (contextPercent === null || contextPercent === undefined || typeof contextPercent !== 'number') {
    return { action: 'skip' };
  }

  // Still in warm-up period
  if (state.toolUseCount <= WARMUP_COUNT) {
    return { action: 'skip' };
  }

  // Below warning threshold
  if (contextPercent < thresholds.warnPercent) {
    return { action: 'skip' };
  }

  // Determine severity
  const severity = contextPercent >= thresholds.criticalPercent ? 'critical' : 'warning';

  // Critical always fires (bypasses debounce)
  if (severity === 'critical') {
    return {
      action: 'warn',
      message: buildWarningMessage(contextPercent, severity),
      severity,
    };
  }

  // Warning: apply debounce
  if (state.debounceCounter < DEBOUNCE_COUNT) {
    return { action: 'skip' };
  }

  return {
    action: 'warn',
    message: buildWarningMessage(contextPercent, severity),
    severity,
  };
}

// ─── Freshness Check ────────────────────────────────────────────────────────

/**
 * Check if bridge data is fresh (within FRESHNESS_WINDOW_SEC).
 * @param {object} bridgeData
 * @returns {boolean}
 */
function isFresh(bridgeData) {
  if (!bridgeData || typeof bridgeData.timestamp !== 'number') return false;
  const nowSec = Math.floor(Date.now() / 1000);
  return (nowSec - bridgeData.timestamp) <= FRESHNESS_WINDOW_SEC;
}

// ─── Main Execution ─────────────────────────────────────────────────────────

function main() {
  try {
    // Subagent detection: exit silently for subagents
    const agentDepth = parseInt(process.env.CLAUDE_AGENT_DEPTH || '0', 10);
    if (agentDepth > 0) {
      process.stdout.write('{}');
      return;
    }

    const sessionId = resolveSessionId();
    const bridgeData = readBridgeFile(sessionId);

    // Read and increment state
    const state = readState(sessionId);
    state.toolUseCount++;
    state.debounceCounter++;

    // No bridge file or stale data → save state and exit
    if (!bridgeData || !isFresh(bridgeData)) {
      writeState(sessionId, state);
      process.stdout.write('{}');
      return;
    }

    const thresholds = resolveThresholds();
    const result = evaluate(bridgeData, state, thresholds);

    if (result.action === 'warn') {
      // Reset debounce counter on emit
      state.debounceCounter = 0;
      state.lastWarnPercent = bridgeData.contextPercent;
      writeState(sessionId, state);

      const output = { additionalContext: result.message };
      process.stdout.write(JSON.stringify(output));
    } else {
      writeState(sessionId, state);
      process.stdout.write('{}');
    }
  } catch {
    // Always exit cleanly
    process.stdout.write('{}');
  }
}

// ─── Exports & Entry Point ──────────────────────────────────────────────────

module.exports = {
  resolveThresholds,
  resolveSessionId,
  readBridgeFile,
  readState,
  writeState,
  buildWarningMessage,
  evaluate,
  isFresh,
  // Constants exported for testing
  DEFAULT_WARN_PERCENT,
  DEFAULT_CRITICAL_PERCENT,
  DEBOUNCE_COUNT,
  WARMUP_COUNT,
  FRESHNESS_WINDOW_SEC,
};

if (require.main === module) {
  main();
}
