#!/usr/bin/env node
/**
 * sessionStart hook — inject the facts a session should never have to ask for.
 *
 *   1. Are the model lanes bound, and is the picker running the model we recorded?
 *   2. What did the last session leave as the next action?
 *   3. Is any capped context file over its cap right now?
 *
 * All three are deterministic file/state checks. The point of doing them here rather
 * than in a prompt is that a rule asking the model to "check the caps" is a request;
 * this is an observation.
 *
 * Contract: stdin is the sessionStart JSON payload; stdout is
 * {"additional_context": "..."}. This hook is fire-and-forget — Cursor logs the
 * response but never blocks session creation on it. It fails open by design:
 * any error prints an empty object and exits 0.
 */

import { readFileSync, existsSync } from "node:fs";
import { join } from "node:path";

const readAll = () =>
  new Promise((resolve) => {
    let data = "";
    process.stdin.setEncoding("utf8");
    process.stdin.on("data", (c) => (data += c));
    process.stdin.on("end", () => resolve(data));
    setTimeout(() => resolve(data), 2000).unref();
  });

const read = (p) => {
  try {
    return existsSync(p) ? readFileSync(p, "utf8") : null;
  } catch {
    return null;
  }
};

/**
 * Real content lines: not blank, not a heading, not the file's instructional blockquote,
 * not a table rule, not an HTML comment, and not one of the shipped `_None yet._` /
 * `[placeholder]` template rows. Counting boilerplate would report a freshly installed
 * kit as already consuming its caps.
 */
const isPlaceholder = (l) =>
  /^[-|*\s]*_?(none|no )/i.test(l) ||
  /^\|?\s*_?\[/.test(l) ||
  /^-\s*\*\(/.test(l) ||
  /^\|\s*\[/.test(l);

const contentLines = (text) =>
  text
    .split("\n")
    .map((l) => l.trim())
    .filter(
      (l) =>
        l &&
        !l.startsWith("#") &&
        !l.startsWith(">") &&
        !l.startsWith("<!--") &&
        !l.startsWith("*") &&
        !/^\|?[\s|:-]+\|?$/.test(l) &&
        !isPlaceholder(l),
    );

function main(payload) {
  const root = payload?.workspace_roots?.[0] ?? process.cwd();
  const notes = [];

  // ---- 1. model routing -------------------------------------------------
  const routing = read(join(root, "docs", "MODEL_ROUTING.md"));
  if (!routing) {
    notes.push("Routing: docs/MODEL_ROUTING.md is missing — this kit is not fully installed.");
  } else if (/\[LEAD\]|\[STRONG\]|\[MID\]|\[FAST\]/.test(routing)) {
    notes.push(
      "Routing: UNBOUND. Every subagent is still `model: inherit`, so each one costs what this session costs. " +
        "Run /model-routing before delegating.",
    );
  } else {
    const row = routing.match(/^\|\s*\*\*lead\*\*\s*\|\s*`?([^`|]+?)`?\s*\|/m);
    // Exact match after normalising case and separators. Substring matching would treat
    // composer-2.5 and composer-2.5-fast — two different lanes in this kit's own table —
    // as the same model and stay silent on a real downgrade.
    const norm = (s) => s.toLowerCase().replace(/[\s_()]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
    const recorded = row?.[1]?.trim();
    const actual = (payload?.model_id || payload?.model || "").trim();
    if (recorded && actual && norm(recorded) !== norm(actual)) {
      notes.push(
        `Routing: lead-model drift — docs/MODEL_ROUTING.md records "${recorded}" but this session is running "${actual}". ` +
          "Either switch the picker or re-run /model-routing; do not silently proceed on the wrong lane.",
      );
    } else if (recorded) {
      notes.push(`Routing: bound (lead = ${recorded}).`);
    }
  }

  // ---- 2. handoff -------------------------------------------------------
  const handoff = read(join(root, "docs", "HANDOFF.md"));
  if (handoff) {
    const next = handoff.match(/Next smallest action:\*{0,2}\s*(.+)/);
    if (next) notes.push(`Handoff next action: ${next[1].trim()}`);
  }

  // ---- 3. context caps --------------------------------------------------
  const over = [];
  const memory = read(join(root, "docs", "MEMORY.md"));
  if (memory) {
    const n = contentLines(memory).filter((l) => l.startsWith("-") || l.startsWith("|")).length;
    if (n > 60) over.push(`docs/MEMORY.md ${n}/60 entry lines`);
  }
  if (handoff) {
    const n = contentLines(handoff).length;
    if (n > 25) over.push(`docs/HANDOFF.md ${n}/25 lines`);
  }
  const tasks = read(join(root, "docs", "TASKS.md"));
  if (tasks) {
    const active = tasks.split(/^##\s+/m).find((s) => /^Active\b/i.test(s));
    if (active) {
      const n = (active.match(/^###\s+T-/gm) || []).length;
      if (n > 7) over.push(`docs/TASKS.md Active ${n}/7 contracts`);
    }
  }
  const agents = read(join(root, "AGENTS.md"));
  if (agents) {
    const section = agents.split(/^##\s+Lessons\s*$/m)[1];
    if (section) {
      const rules = section.split(/^##\s/m)[0].split("\n").filter((l) => /^\s*-\s+\S/.test(l) && !l.includes("*(Add one-line"));
      if (rules.length > 12) over.push(`AGENTS.md ## Lessons ${rules.length}/12 rules`);
    }
  }
  if (over.length) {
    notes.push(`Context caps EXCEEDED: ${over.join(" · ")}. Run /memory-sync before adding anything.`);
  }

  return notes.length
    ? { additional_context: `Project state (from .cursor/hooks/session-context.mjs):\n- ${notes.join("\n- ")}` }
    : {};
}

const raw = await readAll();
let out = {};
try {
  out = main(raw ? JSON.parse(raw) : {});
} catch {
  out = {};
}
process.stdout.write(JSON.stringify(out));
process.exit(0);
