/**
 * Snippet-insertion text math shared by the editor scaffolds (docs/architecture/08 →
 * editor augmentation). The scaffold services (`spec-transform-scaffold`,
 * `spec-param-scaffold`) splice `${n:default}` snippet templates into the spec text;
 * a wrong offset or comma here writes invalid JSON into the user's editor, so the
 * computations are pure functions of the text — `(text, site) → { offset, snippet }` —
 * and table-tested here rather than living inside the Monaco glue. `snippetToPlain`
 * strips a template to its default text so every computed edit (and every catalog
 * skeleton) can be round-tripped through `JSON.parse` in tests.
 */

/** A byte range into the spec text. */
interface ByteRange {
  offset: number;
  length: number;
}

/** A computed text edit: the snippet template to insert and where. */
export interface SnippetEdit {
  offset: number;
  snippet: string;
}

/**
 * The plain form of a snippet: strip tab stops down to their default text
 * (`${1:price}` → `price`; the default-less `${0}` and bare `$0` → ''). Keeps the
 * catalogs and the computed edits testable as valid JSON without a Monaco snippet engine.
 */
export function snippetToPlain(snippet: string): string {
  return snippet
    .replace(
      /\$\{(\d+)(?::([^}]*))?\}/g,
      (_m: string, _n: string, defText?: string) => defText ?? '',
    )
    .replace(/\$\d+/g, '');
}

/** The whitespace-skipping neighbour of an offset, in the given direction (or '' at the edge). */
function neighbour(text: string, from: number, step: -1 | 1): string {
  let i = from;
  while (i >= 0 && i < text.length && /\s/.test(text[i])) i += step;
  return i >= 0 && i < text.length ? text[i] : '';
}

/**
 * Leading/trailing commas needed to splice a new element in at `[start, end)` of a
 * JSON array: a comma before it unless the preceding element already ends in one (or
 * it is the first), and after it unless the following char closes the array (or a
 * comma is already there). Keeps the array valid whether the slot is empty, between
 * elements, or appended after one with no trailing comma.
 */
export function arrayAffixes(
  text: string,
  start: number,
  end: number,
): { lead: string; trail: string } {
  const prev = neighbour(text, start - 1, -1);
  const next = neighbour(text, end, 1);
  const lead = prev !== '' && prev !== '[' && prev !== ',' ? ', ' : '';
  const trail = next !== '' && next !== ']' && next !== ',' ? ',' : '';
  return { lead, trail };
}

/**
 * The edit that appends `entry` at the end of the JSON array spanning `array`:
 * inserted just before the closing `]`, comma-affixed so the array stays valid
 * whether it is empty or already holds elements.
 */
export function appendEntryEdit(text: string, array: ByteRange, entry: string): SnippetEdit {
  const offset = array.offset + array.length - 1;
  const { lead, trail } = arrayAffixes(text, offset, offset);
  return { offset, snippet: `${lead}${entry}${trail}` };
}

/**
 * The edit that creates `"key": [content]` as the first property of the object whose
 * opening brace sits at `braceOffset`. The array is written **inline** — no interior
 * newlines for a snippet engine to re-base — indented one `tabSize` past the brace
 * *line's* leading whitespace, not the brace column, so an object a compact formatter
 * kept mid-line doesn't over-indent. A comma separates it from a following property;
 * an otherwise-empty object takes none, so the result stays valid JSON.
 */
export function createArrayPropertyEdit(
  text: string,
  braceOffset: number,
  tabSize: number,
  key: string,
  content: string,
): SnippetEdit {
  const lineStart = text.lastIndexOf('\n', braceOffset - 1) + 1;
  const baseIndent = /^\s*/.exec(text.slice(lineStart, braceOffset))?.[0].length ?? 0;
  const indent = ' '.repeat(baseIndent + tabSize);
  const trail = neighbour(text, braceOffset + 1, 1) === '}' ? '' : ',';
  return { offset: braceOffset + 1, snippet: `\n${indent}"${key}": [${content}]${trail}` };
}
