#!/usr/bin/env node
/**
 * lint-voice.mjs — deterministic voice linter for Oleg's texts.
 *
 * Checks for AI/corporate artifacts, wrong typography, and banned patterns
 * that make text sound like a model rather than Oleg.
 *
 * Usage:
 *   node lint-voice.mjs "text to check"
 *   echo "text" | node lint-voice.mjs
 *   node lint-voice.mjs < input.txt
 *
 * Exit codes:
 *   0 — clean (no ERRORs; WARNs alone do not fail)
 *   1 — ERRORs found (agent must fix and re-run)
 *   2 — usage error
 */

// [level, id, pattern, message]
// level: "ERROR" = must fix before delivery; "WARN" = check in context, may be OK
const CHECKS = [
  // ── Typography ─────────────────────────────────────────────────────────────
  ["ERROR", "EM_DASH",
    /—/g,
    "Em-dash (—). Replace with colon, comma, or rephrase as separate sentence."],

  ["ERROR", "SMART_QUOTES",
    /[""„«»]/g,
    'Smart/typographic double quote. Use straight " or rephrase.'],

  ["WARN", "SMART_APOSTROPHE",
    /[''`]/g,
    "Smart/curly apostrophe or backtick. Use straight apostrophe '."],

  // ── AI / corporate words ───────────────────────────────────────────────────
  ["ERROR", "AI_LEVERAGE",
    /\bleverag(?:e|es|ed|ing)\b/gi,
    '"leverage" — AI/corporate word. Replace with plain verb (use, apply, build on).'],

  ["ERROR", "AI_ROBUST",
    /\brobust\b/gi,
    '"robust" — AI/corporate word. Replace with specific description.'],

  ["ERROR", "AI_SEAMLESS",
    /\bseamless(?:ly)?\b/gi,
    '"seamless" — AI/corporate word. Cut or replace with plain description.'],

  ["ERROR", "AI_PASSIONATE",
    /\bpassionat(?:e|ely)\b|\bmy passion\b/gi,
    '"passionate/passion" — AI/corporate word. Replace with direct statement.'],

  ["ERROR", "AI_THRILLED",
    /\bthrilled\b/gi,
    '"thrilled" — AI word. Replace with direct statement.'],

  ["ERROR", "AI_DELVE",
    /\bdelv(?:e|ing|ed)\b/gi,
    '"delve" — AI word. Replace with explore / look into / dig.'],

  ["ERROR", "AI_TAPESTRY",
    /\btapestry\b/gi,
    '"tapestry" — AI word. Remove or replace.'],

  ["ERROR", "AI_EMPOWER",
    /\bempower(?:s|ed|ing)?\b/gi,
    '"empower" — AI/corporate word. Replace with plain verb.'],

  ["ERROR", "AI_STREAMLINE",
    /\bstreamline[ds]?\b|\bstreamlining\b/gi,
    '"streamline" — AI/corporate word. Replace with plain verb.'],

  ["ERROR", "AI_ACTIONABLE",
    /\bactionable\b/gi,
    '"actionable" — AI/corporate word. Replace with plain description.'],

  ["ERROR", "AI_UNLOCK",
    /\bunlock(?:s|ed|ing)?\b/gi,
    '"unlock" — AI/corporate word. Replace with plain verb.'],

  ["ERROR", "AI_CUTTING_EDGE",
    /cutting[- ]edge/gi,
    '"cutting-edge" — AI/corporate phrase. Replace with specific description.'],

  ["WARN", "AI_INNOVATIVE",
    /\binnovativ(?:e|ely)\b|\binnovation\b/gi,
    '"innovative/innovation" — often AI/corporate filler. Replace with specific if used generically.'],

  // ── Sentence openers (capitalised opener form) ────────────────────────────
  // Matches word at line start OR after terminal punctuation + whitespace
  ["ERROR", "OPENER_FURTHERMORE",
    /(?:^|(?<=[.!?])[ \t]+)Furthermore[,\s]/gm,
    '"Furthermore" opener. Cut — connection should come from content, not connective.'],

  ["ERROR", "OPENER_MOREOVER",
    /(?:^|(?<=[.!?])[ \t]+)Moreover[,\s]/gm,
    '"Moreover" opener. Cut — restructure.'],

  ["ERROR", "OPENER_ADDITIONALLY",
    /(?:^|(?<=[.!?])[ \t]+)Additionally[,\s]/gm,
    '"Additionally" opener. Cut — restructure without connective.'],

  ["ERROR", "OPENER_ULTIMATELY",
    /(?:^|(?<=[.!?])[ \t]+)Ultimately[,\s]/gm,
    '"Ultimately" opener. Cut — restructure.'],

  ["ERROR", "OPENER_HOWEVER",
    /(?:^|(?<=[.!?])[ \t]+)However[,\s]/gm,
    '"However" opener. Cut — use contrast from content, not a gluing connective.'],

  ["ERROR", "OPENER_IMPORTANTLY",
    /(?:^|(?<=[.!?])[ \t]+)Importantly[,\s]/gm,
    '"Importantly" opener — filler frame. Cut.'],

  ["ERROR", "OPENER_THAT_SAID",
    /(?:^|(?<=[.!?])[ \t]+)That said[,\s]/gm,
    '"That said" opener. Cut — restructure.'],

  ["ERROR", "OPENER_IN_CONCLUSION",
    /in conclusion[,\s]/gi,
    '"In conclusion" — empty closing summary. Cut entirely.'],

  ["ERROR", "OPENER_OVERALL",
    /(?:^|(?<=[.!?])[ \t]+)Overall[,\s]/gm,
    '"Overall" opener — empty summary signal. Cut.'],

  ["WARN", "OPENER_SO_COMMA",
    /(?:^|(?<=[.!?])[ \t]+)So[,\s]/gm,
    '"So," opener. Oleg cuts these — connection should come from content.'],

  // ── Filler frames ──────────────────────────────────────────────────────────
  ["ERROR", "FILLER_WORTH_NOTING",
    /it'?s worth noting/gi,
    '"It\'s worth noting" — filler frame. Cut, say the thing directly.'],

  ["ERROR", "FILLER_IMPORTANT_REMEMBER",
    /it'?s important to remember/gi,
    '"It\'s important to remember" — filler frame. Cut.'],

  ["ERROR", "FILLER_I_AM_WRITING",
    /\bI am writing to\b/gi,
    '"I am writing to" — generic opener. Replace with direct lead.'],

  ["ERROR", "FILLER_IN_TODAYS",
    /in today'?s\b/gi,
    '"In today\'s..." — AI/generic opener. Cut.'],

  // ── AI structural patterns ─────────────────────────────────────────────────
  ["ERROR", "PATTERN_ITS_NOT_JUST",
    /it'?s not just/gi,
    '"it\'s not just X, it\'s Y" construction. Rephrase as direct statement.'],

  ["WARN", "PATTERN_NOT_X_BUT",
    /\bnot \w+(?:\s+\w+){0,3},?\s+but\b/gi,
    '"not X, but Y" — often AI phrasing. Rephrase if rhetorical.'],

  ["ERROR", "PATTERN_HONEST_FRAMING",
    /honest framing|one thing to flag|I want to be honest about/gi,
    '"honest framing / one thing to flag" template. Not Oleg\'s pattern — remove.'],

  // ── AI closings ────────────────────────────────────────────────────────────
  ["ERROR", "CLOSING_LOOKING_FORWARD",
    /looking forward to/gi,
    '"Looking forward to" closing. Replace e.g. "Open to chat if it\'s a fit."'],

  ["ERROR", "CLOSING_HAPPY_TO",
    /\bhappy to\b/gi,
    '"happy to" — AI phrasing. Replace with plain statement.'],

  // ── Warn-only borderline cases ─────────────────────────────────────────────
  ["WARN", "WARN_NAVIGATE",
    /\bnavigat(?:e|ing|ed)\b/gi,
    '"navigate" — check if metaphorical AI-speak ("navigate challenges"). OK if literal.'],

  ["WARN", "WARN_LANDSCAPE",
    /\blandscape\b/gi,
    '"landscape" — check if metaphorical AI-speak. OK if literal/technical.'],
];

function lineOf(text, pos) {
  return text.slice(0, pos).split("\n").length;
}

function getContext(text, start, end, window = 60) {
  const from = Math.max(0, start - window);
  const to = Math.min(text.length, end + window);
  let snippet = text.slice(from, to).replace(/\s+/g, " ").trim();
  if (from > 0) snippet = "…" + snippet;
  if (to < text.length) snippet = snippet + "…";
  return snippet;
}

function runChecks(text) {
  const findings = [];
  for (const [level, id, pattern, message] of CHECKS) {
    // Reset lastIndex before each use (patterns are defined with /g)
    pattern.lastIndex = 0;
    let m;
    while ((m = pattern.exec(text)) !== null) {
      findings.push({
        level,
        id,
        line: lineOf(text, m.index),
        context: getContext(text, m.index, m.index + m[0].length),
        message,
      });
      // Prevent infinite loops on zero-width matches
      if (m[0].length === 0) pattern.lastIndex++;
    }
  }
  findings.sort((a, b) => a.line - b.line || a.id.localeCompare(b.id));
  return findings;
}

async function readStdin() {
  const chunks = [];
  for await (const chunk of process.stdin) chunks.push(chunk);
  return Buffer.concat(chunks).toString("utf8");
}

async function main() {
  let text;

  if (process.argv.length > 2) {
    text = process.argv.slice(2).join(" ");
  } else if (!process.stdin.isTTY) {
    text = await readStdin();
  } else {
    process.stderr.write('Usage: node lint-voice.mjs "text" OR pipe text via stdin\n');
    process.exit(2);
  }

  if (!text.trim()) {
    process.stdout.write("✓ CLEAN — empty input.\n");
    process.exit(0);
  }

  const findings = runChecks(text);
  const errors = findings.filter(f => f.level === "ERROR");
  const warns  = findings.filter(f => f.level === "WARN");

  if (findings.length === 0) {
    process.stdout.write("✓ CLEAN — no issues found.\n");
    process.exit(0);
  }

  process.stdout.write(`Found ${findings.length} issue(s): ${errors.length} ERROR, ${warns.length} WARN\n\n`);

  for (const f of findings) {
    const action = f.level === "ERROR" ? "Fix" : "Note";
    process.stdout.write(`[${f.level}] ${f.id} — line ${f.line}\n`);
    process.stdout.write(`  Context: "${f.context}"\n`);
    process.stdout.write(`  ${action}: ${f.message}\n\n`);
  }

  process.stdout.write("---\n");
  if (errors.length > 0) {
    process.stdout.write(`EXIT 1 — ${errors.length} error(s) present. Fix and re-run.\n`);
    process.exit(1);
  } else {
    process.stdout.write("EXIT 0 — no errors (warnings only). Review WARNs if applicable.\n");
    process.exit(0);
  }
}

main();
