/**
 * Deterministic engine for the §B/§C1 mechanical binding-gate.
 *
 * Pure string/regex evaluation of a draft against a structured ruleset. No AI,
 * no network, no external state — so it is genuinely BINDING: it cannot be
 * skipped or talked around, and it runs in milliseconds. Mechanical violations
 * are caught here BEFORE the draft reaches KTG, so the same correction never
 * reaches him twice.
 *
 * Matching runs on a MASKED copy of the draft (frontmatter + code stripped to
 * spaces, line positions preserved) so identifiers/code never false-positive.
 */

import { readFileSync } from "node:fs";
import type { GateResult, Match, Rule, Violation } from "./types.js";
import { RULES } from "./rules.js";

export { RULES } from "./rules.js";

/**
 * Replace YAML frontmatter, fenced code blocks, and inline code spans with
 * spaces — preserving newlines, length, and therefore every offset/line number.
 */
export function mask(text: string): string {
  const chars = text.split("");
  const blank = (start: number, end: number) => {
    for (let i = start; i < end && i < chars.length; i++) {
      if (chars[i] !== "\n") chars[i] = " ";
    }
  };

  // YAML frontmatter at the very top: ---\n … \n---
  if (text.startsWith("---\n")) {
    const close = text.indexOf("\n---", 3);
    if (close !== -1) {
      const lineEnd = text.indexOf("\n", close + 1);
      blank(0, lineEnd === -1 ? text.length : lineEnd);
    }
  }

  let m: RegExpExecArray | null;
  const fence = /```[\s\S]*?```/g;
  while ((m = fence.exec(text)) !== null) blank(m.index, m.index + m[0].length);

  const inline = /`[^`\n]*`/g;
  while ((m = inline.exec(text)) !== null) blank(m.index, m.index + m[0].length);

  return chars.join("");
}

function lineStarts(text: string): number[] {
  const starts = [0];
  for (let i = 0; i < text.length; i++) {
    if (text[i] === "\n") starts.push(i + 1);
  }
  return starts;
}

/** 1-based line of a character index. */
function lineOf(starts: number[], index: number): number {
  let lo = 0;
  let hi = starts.length - 1;
  let ans = 0;
  while (lo <= hi) {
    const mid = (lo + hi) >> 1;
    if (starts[mid] <= index) {
      ans = mid;
      lo = mid + 1;
    } else {
      hi = mid - 1;
    }
  }
  return ans + 1;
}

function excerptAt(original: string, index: number, len: number): string {
  const start = Math.max(0, index - 20);
  const end = Math.min(original.length, index + len + 20);
  return original.slice(start, end).replace(/\s+/g, " ").trim();
}

function countWords(masked: string): number {
  const m = masked.match(/[A-Za-zÆØÅæøå0-9]+/g);
  return m ? m.length : 0;
}

interface Ctx {
  masked: string;
  original: string;
  starts: number[];
}

function literalMatches(rule: Rule, ctx: Ctx): Match[] {
  const hay = ctx.masked.toLowerCase();
  const out: Match[] = [];
  for (const pat of rule.patterns) {
    const needle = pat.toLowerCase();
    if (!needle) continue;
    let from = 0;
    let idx: number;
    while ((idx = hay.indexOf(needle, from)) !== -1) {
      out.push({
        line: lineOf(ctx.starts, idx),
        excerpt: excerptAt(ctx.original, idx, needle.length),
      });
      from = idx + needle.length;
    }
  }
  return out;
}

/** Run a rule's regex patterns; returns ALL matches (after the allow-filter). */
function regexMatches(rule: Rule, ctx: Ctx, cap = Infinity): { matches: Match[]; count: number } {
  const baseFlags = rule.flags ?? "gi";
  const flags = baseFlags.includes("g") ? baseFlags : baseFlags + "g";
  const matches: Match[] = [];
  let count = 0;
  for (const pat of rule.patterns) {
    const re = new RegExp(pat, flags);
    let m: RegExpExecArray | null;
    while ((m = re.exec(ctx.masked)) !== null) {
      if (m[0].length === 0) {
        re.lastIndex++;
        continue;
      }
      const trimmed = m[0].trim();
      count++;
      if (matches.length < cap) {
        const lead = m[0].length - m[0].trimStart().length;
        const at = m.index + lead;
        matches.push({
          line: lineOf(ctx.starts, at),
          excerpt: excerptAt(ctx.original, at, trimmed.length),
        });
      }
    }
  }
  return { matches, count };
}

function evalRule(rule: Rule, ctx: Ctx, wordCount: number): Violation | null {
  let matches: Match[] = [];
  let observed: number | undefined;
  let threshold: number | undefined;

  switch (rule.kind) {
    case "literal":
      matches = literalMatches(rule, ctx);
      break;
    case "regex":
      matches = regexMatches(rule, ctx).matches;
      break;
    case "count": {
      const { matches: ms, count } = regexMatches(rule, ctx, 8);
      observed = count;
      threshold = rule.threshold ?? 0;
      if (count > threshold) matches = ms;
      break;
    }
    case "density": {
      const { matches: ms, count } = regexMatches(rule, ctx, 8);
      const maxAllowed = Math.floor((wordCount / (rule.per ?? 50)) * (rule.threshold ?? 1));
      observed = count;
      threshold = maxAllowed;
      if (count > maxAllowed) matches = ms;
      break;
    }
  }

  if (matches.length === 0) return null;
  return {
    ruleId: rule.id,
    ref: rule.ref,
    severity: rule.severity,
    message: rule.message,
    matches,
    observed,
    threshold,
  };
}

/** Evaluate raw draft text against a ruleset. */
export function evaluate(
  text: string,
  rules: Rule[] = RULES,
  draftPath: string | null = null,
): GateResult {
  const masked = mask(text);
  const ctx: Ctx = { masked, original: text, starts: lineStarts(text) };
  const wordCount = countWords(masked);

  const blocks: Violation[] = [];
  const warns: Violation[] = [];
  for (const rule of rules) {
    const v = evalRule(rule, ctx, wordCount);
    if (!v) continue;
    (v.severity === "block" ? blocks : warns).push(v);
  }

  return { draftPath, wordCount, blocks, warns, passed: blocks.length === 0 };
}

/** Read a draft file from disk and evaluate it. */
export function runGateFile(path: string, rules: Rule[] = RULES): GateResult {
  const text = readFileSync(path, "utf8");
  return evaluate(text, rules, path);
}
