/**
 * Tiny rich-text format used for the report narrative and the clause evidence cells.
 * The auditor sets it from the frontend toolbar (B / I / U / List buttons or Ctrl+B / Ctrl+I / Ctrl+U).
 *
 *    plain line                      -> normal paragraph
 *    • text   (or "- text")          -> bullet item
 *    **bold**                        -> bold run
 *    *italic*                        -> italic run (a single "*" only counts when a closing "*" follows on the same line)
 *    __underline__                   -> underlined run
 *    **__heading__**                 -> bold + underlined run
 *    ***bold italic***, **__*all*__** -> markers can be combined freely
 *
 * A line whose runs are all bold (and is not a bullet) is a heading: heading = true.
 */
export interface RichRun { t: string; b?: boolean; i?: boolean; u?: boolean }
export interface RichBlock { bullet: boolean; heading?: boolean; runs: RichRun[] }

export function parseRich(text: string | null | undefined): RichBlock[] {
  if (!text) return [];
  const out: RichBlock[] = [];

  for (const raw of String(text).replace(/\r/g, '').split('\n')) {
    let line = raw.trim();
    if (!line) continue;

    let bullet = false;
    const m = line.match(/^(?:[•●▪‣]|[-*])\s+(.*)$/);
    if (m) {
      bullet = true;
      line = m[1].trim();
    }

    // tokens: "**" (bold), "__" (underline), single "*" (italic)
    const toks: { at: number; len: number; kind: 'b' | 'u' | 'i' }[] = [];
    const re = /(\*\*|__|\*)/g;
    let mk: RegExpExecArray | null;
    while ((mk = re.exec(line)) !== null) {
      toks.push({ at: mk.index, len: mk[0].length, kind: mk[0] === '**' ? 'b' : mk[0] === '__' ? 'u' : 'i' });
    }
    // a lone "*" (e.g. "5 * 3", "ISO 9001*") stays literal text: italic markers only count in pairs
    const italics = toks.filter((k) => k.kind === 'i');
    const literal = new Set(italics.length % 2 ? [italics[italics.length - 1]] : []);

    const runs: RichRun[] = [];
    let bold = false;
    let ital = false;
    let under = false;
    let last = 0;

    const push = (t: string) => {
      if (!t) return;
      const prev = runs[runs.length - 1];
      // merge with the previous run when the style is the same (keeps literal "*" in one run)
      if (prev && !!prev.b === bold && !!prev.i === ital && !!prev.u === under) { prev.t += t; return; }
      runs.push({
        t,
        ...(bold ? { b: true } : {}),
        ...(ital ? { i: true } : {}),
        ...(under ? { u: true } : {}),
      });
    };

    for (const k of toks) {
      if (literal.has(k)) continue; // left in the text
      push(line.slice(last, k.at));
      if (k.kind === 'b') bold = !bold;
      else if (k.kind === 'u') under = !under;
      else ital = !ital;
      last = k.at + k.len;
    }
    push(line.slice(last));

    if (!runs.length) continue;

    const heading = !bullet && runs.every((r) => r.b) && runs.some((r) => r.t.trim() !== '');
    out.push({ bullet, heading, runs });
  }

  return out;
}

/** plain text of a block list (used for searching / AI context) */
export const richToPlain = (blocks: RichBlock[]) =>
  blocks.map((b) => b.runs.map((r) => r.t).join('')).join('\n');

/** raw text with the **, __ and paired * markers removed (for places that can only print plain text) */
export const stripRich = (text: string | null | undefined): string =>
  String(text ?? '')
    .replace(/\r/g, '')
    .split('\n')
    .map((ln) => {
      const blk = parseRich(ln)[0];
      if (!blk) return ln.trim();
      const body = blk.runs.map((r) => r.t).join('');
      return blk.bullet ? `• ${body}` : body;
    })
    .join('\n');
