/**
 * # format — the canonical formatter (text → text), built on print ∘ parse
 *
 * ADR-0004 makes the round-trip the definition of done: `print(parse(text))` is the byte-
 * stable canonical form, so **formatting simply IS `print ∘ parse`** — there is no separate
 * formatting logic to drift from the grammar. Because the canonical form is a fixed point,
 * {@link format} is idempotent (`format(format(x)) === format(x)`).
 *
 * Fail closed (ARCHITECTURE §6): text that does not parse is returned UNCHANGED rather than
 * mangled. A formatter that ate an in-progress buffer — or rewrote a deliberately-invalid
 * `reject` fixture in the docs — would be worse than useless, so a {@link KestrelParseError}
 * degrades to "leave it exactly as the author wrote it". Non-parse errors (genuine bugs)
 * still propagate loudly.
 *
 * Portability: imports ONLY `src/lang` (the parse/print barrel) and pure TS. No `node:*`, no
 * DOM. The Markdown fence walk is a self-contained line scanner (it adapts the gsh.1 fence
 * pattern from tests/support/fences.ts, but that module is node-bound, so it is not imported).
 */

import { KestrelParseError, parse, print } from "../lang/index.ts";

/**
 * Canonicalize a whole `.kestrel` document. Returns `print(parse(text))` — byte-stable and
 * idempotent. Fails closed: if the text does not parse, it is returned unchanged.
 */
export function format(text: string): string {
  try {
    return print(parse(text));
  } catch (e) {
    if (e instanceof KestrelParseError) return text; // fail closed — never mangle
    throw e;
  }
}

/** One ` ```kestrel ` fenced block located in a Markdown source, with the absolute offsets of
 * its body so a caller can splice a replacement or offset highlight tokens into it. */
export interface KestrelBlock {
  /** 0-based offset of the first character of the body (start of the line after the fence). */
  readonly bodyStart: number;
  /** 0-based offset one past the last body character (`bodyStart + body.length`). */
  readonly bodyEnd: number;
  /** The body exactly as written (the fence lines excluded, no surrounding newlines). */
  readonly body: string;
}

const FENCE_OPEN = /^(\s*)(`{3,})(.*)$/;
const FENCE_CLOSE = /^(\s*)(`{3,})\s*$/;

/**
 * Locate every ` ```kestrel ` fenced block in a Markdown/MDX source, in order. Only fences
 * whose info-string language token is exactly `kestrel` are returned — a `text`/`kdl`/… fence
 * (how the docs mark `reject`/`proposed` examples, per tests/support/fences.ts) is skipped, so
 * deliberately-invalid syntax is never touched. An unterminated fence is skipped (fail closed).
 *
 * Shared by {@link formatMarkdown} (to splice canonical bodies) and the editor semantic-token
 * provider (to offset highlight tokens into a Markdown document).
 */
export function kestrelBlocks(text: string): KestrelBlock[] {
  const lines = text.split("\n");
  const starts: number[] = [];
  let off = 0;
  for (const l of lines) {
    starts.push(off);
    off += l.length + 1; // + newline (the final line's phantom newline is never read)
  }

  const blocks: KestrelBlock[] = [];
  let i = 0;
  while (i < lines.length) {
    const open = FENCE_OPEN.exec(lines[i]!);
    // A backtick fence's info string may not contain backticks (CommonMark §4.5): a line with
    // an inline code span is not a fence opener. Skip it (matches tests/support/fences.ts).
    if (open !== null && !open[3]!.includes("`")) {
      const lang = /^\S*/.exec(open[3]!.trim())![0];
      if (lang === "kestrel") {
        const ticks = open[2]!;
        let c = i + 1;
        let closed = false;
        for (; c < lines.length; c++) {
          const close = FENCE_CLOSE.exec(lines[c]!);
          if (close !== null && close[2]!.length >= ticks.length) {
            closed = true;
            break;
          }
        }
        if (closed) {
          const body = lines.slice(i + 1, c).join("\n");
          const bodyStart = starts[i + 1]!; // i+1 ≤ c ≤ lines.length-1 ⇒ always in range
          blocks.push({ bodyStart, bodyEnd: bodyStart + body.length, body });
          i = c + 1;
          continue;
        }
      }
    }
    i++;
  }
  return blocks;
}

/**
 * Format every parseable ` ```kestrel ` fence in a Markdown/MDX document in place, leaving the
 * surrounding prose, the fence markers, and every un-parseable or non-`kestrel` fence byte-for-
 * byte untouched (fail closed). Because {@link format} returns its input unchanged on a parse
 * error, a `reject`/`proposed`/typo fence is preserved exactly — this is a pure canonicaliza-
 * tion of the executable examples the docs assert (ADR-0001).
 */
export function formatMarkdown(text: string): string {
  const blocks = kestrelBlocks(text);
  let out = "";
  let cursor = 0;
  for (const b of blocks) {
    out += text.slice(cursor, b.bodyStart);
    const formatted = format(b.body);
    // format() already fails closed to `b.body`; comparing keeps an unchanged body byte-exact.
    out += formatted === b.body ? b.body : formatted;
    cursor = b.bodyEnd;
  }
  out += text.slice(cursor);
  return out;
}
