/**
 * Rich text as a *serialisable value*, not opaque HTML.
 *
 * A block is a node tree. Because a placeholder is a first-class NODE (not a
 * `{{token}}` substring hidden inside a string), highlighting it, inserting it,
 * and linting it are all pure functions over data — no regex over markup. This
 * is the whole reason the letter feature stays in the "impossible states" style:
 * the value the app holds is always well-shaped, and the imperative editor is
 * quarantined behind one component that converts to/from this tree.
 */

export type Mark = 'bold' | 'italic' | 'underline';

export type RichTextNode =
  | { readonly type: 'text'; readonly text: string; readonly marks?: readonly Mark[] }
  | { readonly type: 'placeholder'; readonly key: string } // resolved to a value at send
  | { readonly type: 'lineBreak' };

export interface Paragraph {
  readonly nodes: readonly RichTextNode[];
  // A line can be a plain paragraph (undefined) or an item in a bullet/numbered list.
  // Consecutive lines with the same list kind render as one <ul>/<ol>.
  readonly list?: 'bullet' | 'number';
}

export interface RichTextBlock {
  readonly paragraphs: readonly Paragraph[];
}

/** An empty editable block is one empty paragraph — never zero paragraphs, so the
    editor always has a caret line. */
export function emptyBlock(): RichTextBlock {
  return { paragraphs: [{ nodes: [] }] };
}

/** True when the block carries no visible content (used for "required section empty"). */
export function isBlockEmpty(block: RichTextBlock): boolean {
  return block.paragraphs.every((p) =>
    p.nodes.every((n) => (n.type === 'text' ? n.text.trim() === '' : false)),
  );
}

/** The frozen-snapshot primitive: a deep VALUE copy of a block. Inserting a library
    passage into a letter copies its tree through here, so the letter never shares a
    reference with the library — later library edits can't mutate an existing letter. */
export function deepCopyBlock(block: RichTextBlock): RichTextBlock {
  // ponytail: structuredClone is exactly a deep value copy of a JSON-shaped tree;
  // a hand-rolled walk would be more code for the same result.
  return structuredClone(block) as RichTextBlock;
}

/** Every placeholder key used in a block, in document order (duplicates kept — the
    caller dedupes when it wants a set). */
export function placeholderKeysIn(block: RichTextBlock): string[] {
  const keys: string[] = [];
  for (const p of block.paragraphs) {
    for (const n of p.nodes) {
      if (n.type === 'placeholder') keys.push(n.key);
    }
  }
  return keys;
}
