import { Injectable, Logger } from '@nestjs/common';
import * as fs from 'fs';
import * as path from 'path';
import {
  AlignmentType, BorderStyle, Document, Footer, Header, HorizontalPositionRelativeFrom, ImageRun, Packer,
  PageNumber, Paragraph, ShadingType, Table, TableCell, TableLayoutType, TableRow, TextRun, VerticalAlign,
  VerticalPositionRelativeFrom, WidthType, TextWrappingType, PageBreak, HeightRule, LevelFormat, UnderlineType,
} from 'docx';
import { ReportRenderService } from './report-render.service';
import { ReportImageService } from './report-image.service';
import { RichBlock } from './rich-text.util';
import { imsIconFor, profileFor, ReportTheme } from '../report.standards';

/** A4 portrait with 900-twip side margins → 10106 twips of usable width (same as the Word form). */
const W = 10106;
const BLUE = '4C94D8';
const NAVY = '0A2A5E';
const FONT = 'Calibri';
const SYM = 'Segoe UI Symbol';
const SZ = 18; // 9 pt, same as the Word form
const EVIDENCE_FILL = 'F3F7FC';
const mm2tw = (mm: number) => Math.round(mm * 56.693);

const px = (mm: number) => Math.round((mm / 25.4) * 96);

/** Reads width/height from a PNG or JPEG without extra dependencies. */
function imageSize(buf: Buffer): { w: number; h: number } {
  if (buf.readUInt32BE(0) === 0x89504e47) return { w: buf.readUInt32BE(16), h: buf.readUInt32BE(20) };
  let i = 2;
  while (i < buf.length) {
    if (buf[i] !== 0xff) { i++; continue; }
    const marker = buf[i + 1];
    const len = buf.readUInt16BE(i + 2);
    if (marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker)) {
      return { h: buf.readUInt16BE(i + 5), w: buf.readUInt16BE(i + 7) };
    }
    i += 2 + len;
  }
  return { w: 1, h: 1 };
}

type Cell = {
  text?: string;
  children?: (Paragraph | Table)[];
  fill?: string;
  bold?: boolean;
  color?: string;
  w: number;
  span?: number;
  center?: boolean;
  vMiddle?: boolean;
  size?: number;
};

@Injectable()
export class ReportDocxService {
  private readonly logger = new Logger(ReportDocxService.name);

  constructor(private readonly renderer: ReportRenderService, private readonly images: ReportImageService) {}

  /**
   * Colours of the form being built (blue Form 04-2a / green Form 04-2b). Set at the start of buildFromVm() and read by the
   * small builders below. buildFromVm() builds every paragraph synchronously before its first await, so two reports built
   * at the same time cannot see each other's theme.
   */
  private T: ReportTheme = profileFor('').theme;

  private templateDir(): string {
    return this.renderer.templateDirPublic();
  }
  private img(file: string): Buffer | null {
    const p = path.join(this.templateDir(), file);
    return fs.existsSync(p) ? fs.readFileSync(p) : null;
  }

  // ───────────────────────── small builders ─────────────────────────
  private run(text: string, o: { bold?: boolean; italics?: boolean; color?: string; size?: number; font?: string } = {}) {
    return new TextRun({ text, bold: o.bold, italics: o.italics, color: o.color, size: o.size ?? SZ, font: o.font ?? FONT });
  }
  private p(text: string, o: { bold?: boolean; italics?: boolean; center?: boolean; justify?: boolean; color?: string; size?: number; before?: number; after?: number } = {}) {
    return new Paragraph({
      alignment: o.center ? AlignmentType.CENTER : o.justify ? AlignmentType.JUSTIFIED : AlignmentType.LEFT,
      spacing: { before: o.before ?? 0, after: o.after ?? 40 },
      children: [this.run(text, o)],
    });
  }
  private paras(list: string[] | undefined, o: { justify?: boolean } = {}): Paragraph[] {
    const arr = (list ?? []).filter((x) => x !== undefined && x !== null);
    return arr.length ? arr.map((t) => this.p(String(t), { justify: o.justify, size: o.justify ? 17 : undefined, after: o.justify ? 80 : undefined })) : [this.p('')];
  }
  private richParas(blocks: RichBlock[] | undefined): Paragraph[] {
    if (!blocks?.length) return [this.p('')];
    return blocks.map((b) => new Paragraph({
      numbering: b.bullet ? { reference: 'ev-bullet', level: 0 } : undefined,
      alignment: b.bullet ? AlignmentType.LEFT : AlignmentType.JUSTIFIED,
      // a heading sits directly on the paragraph below it, as in the original Word file
      spacing: { before: 0, after: b.heading ? 0 : 60 },
      keepNext: !!b.heading,
      children: b.runs.map((r) => new TextRun({
        text: String(r.t),
        bold: !!r.b,
        italics: !!r.i,
        underline: r.u ? { type: UnderlineType.SINGLE } : undefined,
        size: 17,
        font: FONT,
      })),
    }));
  }
  /**
   * Narrative section: printed with the auditor's bold / italic / underline / bullets (sections_rich).
   * Older approved snapshots have no sections_rich for the key -> plain paragraphs exactly as before.
   */
  private sectionParas(vm: any, key: string, o: { justify?: boolean } = {}): Paragraph[] {
    const rich = vm?.sections_rich?.[key];
    if (Array.isArray(rich) && rich.length) return this.richParas(rich);
    return this.paras(vm?.sections?.[key], o);
  }
  private box(on: boolean) {
    return this.run(on ? '■' : '☐', { font: SYM, size: SZ });
  }
  /** "☐ Yes  ■ No" style line. */
  private opts(items: { text: string; on: boolean }[]): Paragraph {
    const kids: TextRun[] = [];
    items.forEach((it, i) => {
      if (i) kids.push(this.run('   '));
      kids.push(this.box(it.on), this.run(' ' + it.text));
    });
    return new Paragraph({ spacing: { after: 40 }, children: kids });
  }
  private bar(text: string, pageBreak = false): Paragraph {
    return new Paragraph({
      pageBreakBefore: pageBreak,
      spacing: { before: 260, after: 100 },
      indent: { left: -108 },
      keepNext: true,
      shading: { type: ShadingType.CLEAR, color: 'auto', fill: this.T.primary },
      children: [this.run(text.toUpperCase(), { bold: true, color: this.T.primary_text, size: 21 })],
    });
  }
  private groupHeading(text: string): Paragraph {
    return new Paragraph({ keepNext: true, spacing: { before: 160, after: 80 }, children: [this.run(text, { bold: true, size: 20, color: this.T.group_heading })] });
  }
  private spacer(after = 120) {
    return new Paragraph({ spacing: { after }, children: [] });
  }

  private border(color = '999999') {
    const b = { style: BorderStyle.SINGLE, size: 4, color };
    return { top: b, bottom: b, left: b, right: b };
  }
  private cell(c: Cell): TableCell {
    const children =
      c.children ??
      [
        new Paragraph({
          alignment: c.center ? AlignmentType.CENTER : AlignmentType.LEFT,
          spacing: { after: 40 },
          children: [this.run(c.text ?? '', { bold: c.bold, color: c.color, size: c.size })],
        }),
      ];
    return new TableCell({
      width: { size: c.w, type: WidthType.DXA },
      columnSpan: c.span,
      verticalAlign: c.vMiddle ? VerticalAlign.CENTER : VerticalAlign.TOP,
      shading: c.fill ? { type: ShadingType.CLEAR, color: 'auto', fill: c.fill } : undefined,
      margins: { top: 60, bottom: 60, left: 100, right: 100 },
      borders: this.border(),
      children,
    });
  }
  private lbl(text: string, w: number): Cell {
    return { text, w, fill: this.T.primary, bold: true, color: this.T.primary_text };
  }
  private table(widths: number[], rows: TableRow[]): Table {
    return new Table({
      width: { size: widths.reduce((a, b) => a + b, 0), type: WidthType.DXA },
      columnWidths: widths,
      layout: TableLayoutType.FIXED,
      indent: { size: -108, type: WidthType.DXA },
      rows,
    });
  }
  private row(cells: Cell[], opts: { header?: boolean; cantSplit?: boolean; minH?: number } = {}): TableRow {
    return new TableRow({
      tableHeader: opts.header,
      cantSplit: opts.cantSplit,
      height: opts.minH ? { value: opts.minH, rule: HeightRule.ATLEAST } : undefined,
      children: cells.map((c) => this.cell(c)),
    });
  }
  private headRow(labels: string[], widths: number[]): TableRow {
    return this.row(labels.map((l, i) => ({ text: l, w: widths[i], fill: this.T.primary, bold: true, color: this.T.primary_text, center: true })), { header: true });
  }

  // ───────────────────────── photos uploaded by the auditor ─────────────────────────
  /**
   * Pictures in a borderless grid (N per row, each with its caption under it) - used under "Brief about the
   * organization" (the product pictures of the Word form), under "Audit brief" and inside clause rows.
   */
   private gallery(refs: any[] | undefined, o: { perRow: number; cellTwips: number; heightMm: number }): (Paragraph | Table)[] {
    const list = (refs ?? []).filter(Boolean);
    if (!list.length) return [];
    const nb = { style: BorderStyle.NONE, size: 0, color: 'FFFFFF' };
    const none = { top: nb, bottom: nb, left: nb, right: nb };
    // one image fills the full text width; several images go up to three per row
    const perRow = list.length === 1 ? 1 : Math.min(o.perRow, 3);
    const cellTwips = perRow === 1 ? W - 200 : Math.floor((W - 200) / perRow);
    const maxW = Math.max(40, Math.floor((cellTwips - 240) / 15));
    const maxH = px(list.length === 1 ? 110 : o.heightMm);
    const rows: TableRow[] = [];
    for (let i = 0; i < list.length; i += perRow) {
      const cells: TableCell[] = list.slice(i, i + perRow).map((ref) => {
        const data = this.images.read(ref);
        const kids: Paragraph[] = [];
        if (data) {
          const w0 = Number(ref.w) || 4;
          const h0 = Number(ref.h) || 3;
          let h = maxH;
          let w = Math.round((h * w0) / h0);
          if (w > maxW) { w = maxW; h = Math.round((w * h0) / w0); }
          kids.push(new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 20 }, children: [new ImageRun({ type: data.type, data: data.buffer, transformation: { width: w, height: h } })] }));
        }
        if (ref.caption) kids.push(new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 40 }, children: [this.run(String(ref.caption), { size: 15, bold: true, color: this.T.brand })] }));
        if (!kids.length) kids.push(new Paragraph({ children: [] }));
        return new TableCell({ width: { size: cellTwips, type: WidthType.DXA }, borders: none, margins: { top: 40, bottom: 40, left: 60, right: 60 }, children: kids });
      });
      while (cells.length < perRow) cells.push(new TableCell({ width: { size: cellTwips, type: WidthType.DXA }, borders: none, children: [new Paragraph({ children: [] })] }));
      rows.push(new TableRow({ cantSplit: true, children: cells }));
    }
    return [new Table({
      width: { size: cellTwips * perRow, type: WidthType.DXA },
      columnWidths: Array(perRow).fill(cellTwips),
      layout: TableLayoutType.FIXED,
      borders: { top: nb, bottom: nb, left: nb, right: nb, insideHorizontal: nb, insideVertical: nb },
      rows,
    })];
  }

  /** Photos for a clause, sized to the full row width: one large photo, or two per row for several. */
  private photoBlock(list: any[], innerTwips: number): (Paragraph | Table)[] {
    const perRow = list.length === 1 ? 1 : 2;
    const cellTwips = Math.floor(innerTwips / perRow);
    const heightMm = perRow === 1 ? 110 : 80;
    return this.gallery(list, { perRow, cellTwips, heightMm });
  }

  /** Every image carries its clause number, e.g. "Clause 6.2 — Quality Objectives". */
  private withClauseCaption(refs: any[], code: string): any[] {
    return (refs ?? []).filter(Boolean).map((ref) => ({
      ...ref,
      caption: `Clause ${code}${ref.caption ? ' — ' + ref.caption : ''}`,
    }));
  }

  /**
   * Full-width shaded row under a table row that has photos. Returns null when the row has no photos.
   * `span` is the number of columns the row covers.
   */
  private photoRowFor(r: any, fullW: number, span: number): TableRow | null {
    const photos = (r.images ?? []).filter(Boolean);
    if (!photos.length) return null;
    const code = String(r.code || r.label || '').trim();
    const caption = this.p(`Digital evidence generated for clause ${code}`, { bold: true, size: 16, color: this.T.brand, after: 60 });
    return this.row([
      {
        w: fullW,
        span,
        vMiddle: false,
        fill: this.T.evidence_fill || EVIDENCE_FILL,
        children: [caption, ...this.photoBlock(this.withClauseCaption(photos, code), fullW - 200)],
      },
    ], { cantSplit: true });
  }

  // ───────────────────────── status / evidence text cells ─────────────────────────
  private statusCell(r: any, w: number): Cell {
    const kids: TextRun[] = [];
    [['C', r.C], ['NC', r.NC], ['OFI', r.OFI], ['NA', r.NA]].forEach(([t, on], i) => {
      if (i) kids.push(this.run('  ', { size: 16 }));
      kids.push(this.run(on ? '■' : '☐', { font: SYM, size: 16 }), this.run(' ' + t, { size: 16 }));
    });
    return { w, vMiddle: true, children: [new Paragraph({ spacing: { after: 20 }, children: kids })] };
  }

  /** Objective evidence text only (photos are printed in their own shaded row beneath the table row). */
  private evidenceTextCell(r: any, w: number): Cell {
    const kids: (Paragraph | Table)[] = [];
    (r.evidence ?? []).forEach((blk: any) => {
      const runs = (blk.runs ?? []).map((x: any) => new TextRun({
        text: String(x.t), bold: !!x.b, italics: !!x.i, underline: x.u ? { type: UnderlineType.SINGLE } : undefined,
        size: SZ, font: FONT,
      }));
      if (blk.bullet) {
        kids.push(new Paragraph({ numbering: { reference: 'ev-bullet', level: 0 }, alignment: AlignmentType.LEFT, spacing: { after: 40 }, children: runs }));
      } else {
        kids.push(new Paragraph({ alignment: AlignmentType.JUSTIFIED, spacing: { after: 40 }, children: runs }));
      }
    });
    if (!kids.length) kids.push(this.p(''));
    return { w, vMiddle: false, children: kids };
  }

  // ───────────────────────── main ─────────────────────────
  async build(reportId: number): Promise<{ buffer: Buffer; filename: string }> {
    const { vm, report } = await this.renderer.getViewModel(reportId);
    const buffer = await this.buildFromVm(vm);
    const base = (vm.meta?.report_no || `report-${reportId}`) + (vm.meta?.draft ? '-DRAFT' : '');
    void report;
    return { buffer, filename: `${base}.docx` };
  }

  async buildFromVm(vm: any): Promise<Buffer> {
    this.T = vm.theme ?? profileFor(String(vm.meta?.standard_name ?? '')).theme; // old snapshots carry no theme
    const cv = this.T.cover;
    const cover = this.img(cv.file) ?? this.img('cover.jpg');
    const crest = this.img('crest.png');
    const stripe = this.img('header-stripe.jpg');

    // ── cover section ──
    const coverKids: Paragraph[] = [];
    if (cover) {
      const dim = imageSize(cover);
      const topMm = 12;
      const hPx = px(297 - 2 * topMm);
      const wPx = Math.min(px(210), Math.round((hPx * dim.w) / dim.h));
      coverKids.push(
        new Paragraph({
          children: [
            new ImageRun({
              type: 'jpg',
              data: cover,
              transformation: { width: wPx, height: hPx },
              floating: {
                horizontalPosition: { relative: HorizontalPositionRelativeFrom.PAGE, offset: Math.round(((px(210) - wPx) / 2) * 9525) },
                verticalPosition: { relative: VerticalPositionRelativeFrom.PAGE, offset: px(topMm) * 9525 },
                behindDocument: true,
                wrap: { type: TextWrappingType.NONE },
              },
            }),
          ],
        }),
      );
    }
    const c = vm.cover ?? {};
    const company = String(c.company ?? '').toUpperCase();
    const rnText = `Report No. ${vm.meta?.report_no ?? ''}  |  ${vm.meta?.stage_label ?? ''}`;
    if (cv.kind === 'ims' && cv.top_mm) {
      // IMS cover (samples/IMS_*.docx): crest, FINAL AUDIT REPORT, company, three standard columns, title, white bottom band text
      const crestBig = cv.crest ? this.img(cv.crest) : null;
      if (crestBig) {
        const d = imageSize(crestBig);
        const hh = px(30);
        const ww = Math.round((hh * d.w) / d.h);
        // zero-height paragraph: the picture floats, so the paragraph must not push the text below down
        coverKids.push(new Paragraph({ spacing: { before: 0, after: 0, line: 20, lineRule: 'exact' as any }, children: [new ImageRun({
          type: 'png', data: crestBig, transformation: { width: ww, height: hh },
          floating: {
            horizontalPosition: { relative: HorizontalPositionRelativeFrom.PAGE, offset: Math.round(((px(210) - ww) / 2) * 9525) },
            verticalPosition: { relative: VerticalPositionRelativeFrom.PAGE, offset: px(20) * 9525 },
            wrap: { type: TextWrappingType.NONE },
          },
        })] }));
      }
      let y = 900 + (crestBig ? 20 : 0);
      const at = (mm: number, lines: number, sizeHalfPt: number, kids: TextRun[], o: { align?: any; indent?: number } = {}) => {
        const line = Math.round((sizeHalfPt / 2) * 1.25 * 20);
        const before = Math.max(0, mm2tw(mm) - y);
        coverKids.push(new Paragraph({ alignment: o.align ?? AlignmentType.CENTER, indent: { left: o.indent ?? 500, right: o.indent ?? 500 }, spacing: { before, after: 0, line, lineRule: 'exact' as any }, children: kids }));
        y = mm2tw(mm) + lines * line;
      };
      at(cv.top_mm.label, 1, 42, [this.run(cv.label || 'FINAL AUDIT REPORT', { bold: true, size: 42, color: cv.label_color })]);
      const coSize = company.length > 40 ? 40 : company.length > 26 ? 52 : 72;
      at(cv.top_mm.company, company.length > 26 ? 2 : 1, coSize, [this.run(company, { bold: true, size: coSize, color: cv.company_color })]);
      at(84, 1, 20, [this.run('INTEGRATED MANAGEMENT SYSTEM', { bold: true, size: 20, color: '181717' })]);
      // three columns: standard name (coloured) and, lower, the system name
      const cols: any[] = Array.isArray(c.ims_columns) && c.ims_columns.length ? c.ims_columns : [];
      if (cols.length) {
        const nb = { style: BorderStyle.NONE, size: 0, color: 'FFFFFF' };
        const none = { top: nb, bottom: nb, left: nb, right: nb };
        const cw = Math.floor((W + 600) / cols.length);
        const cellOf = (kids: Paragraph[]) => new TableCell({ width: { size: cw, type: WidthType.DXA }, borders: none, margins: { top: 0, bottom: 0, left: 60, right: 60 }, children: kids });
        const icons = cols.map((k: any) => { const f = imsIconFor(k.main); return f ? this.img(f) : null; });
        const hasIcons = icons.some((b) => !!b);
        const wrapHead = (head: string): string[] => {
          const out: string[] = [];
          for (const w of head.trim().split(/\s+/).filter(Boolean)) {
            if (out.length && (out[out.length - 1] + ' ' + w).length <= 20) out[out.length - 1] += ' ' + w;
            else out.push(w);
          }
          return out.length ? out : [''];
        };
        const maxHeadLines = Math.max(1, ...cols.map((k: any) => wrapHead(String(k.sub ?? '').split(' MANAGEMENT SYSTEM')[0]).length));
        const subRowMm = Math.max(12, (maxHeadLines + 1) * 4.4);
        const before = Math.max(0, mm2tw(cv.top_mm.std) - y);
        coverKids.push(new Paragraph({ spacing: { before, after: 0, line: 20, lineRule: 'exact' as any }, children: [] }));
        coverKids.push(new Table({
          width: { size: cw * cols.length, type: WidthType.DXA }, columnWidths: cols.map(() => cw), layout: TableLayoutType.FIXED, indent: { size: -300, type: WidthType.DXA },
          rows: [
            new TableRow({ height: { value: mm2tw(cv.show_sub || hasIcons ? 8.5 : 22), rule: HeightRule.EXACT }, children: cols.map((k: any) => cellOf([new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 0 }, children: [this.run(String(k.main ?? ''), { bold: true, size: 30, color: String(k.color ?? '181717') })] })])) }),
            // 2026-11-06: round icon under each standard name, as in the sample Word cover (samples/IMS_*.docx)
            ...(hasIcons ? [new TableRow({ height: { value: mm2tw(14), rule: HeightRule.EXACT }, children: cols.map((k: any, i: number) => cellOf([new Paragraph({
              alignment: AlignmentType.CENTER, spacing: { before: 0, after: 0 },
              children: icons[i] ? [new ImageRun({ type: 'png', data: icons[i]!, transformation: { width: px(12.2), height: px(12.2) } })] : [],
            })])) })] : []),
            // 2026-11-06: a long head such as "OCCUPATIONAL HEALTH AND SAFETY" (standards.title of 45001) ran over the
            // cover's frame line — it is now wrapped at ~20 characters so it stays inside its column
            ...(cv.show_sub ? [new TableRow({ height: { value: mm2tw(subRowMm), rule: HeightRule.EXACT }, children: cols.map((k: any) => cellOf(String(k.sub ?? '').split(' MANAGEMENT SYSTEM').filter((_: string, i: number) => i === 0).flatMap((head: string) => [
              ...wrapHead(head).map((ln) => new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 0, line: 240, lineRule: 'exact' as any }, children: [this.run(ln, { bold: true, size: 19, color: '1A1915' })] })),
              new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 0, line: 240, lineRule: 'exact' as any }, children: [this.run(String(k.sub ?? '').includes('MANAGEMENT SYSTEM') ? 'MANAGEMENT SYSTEM' : '', { bold: true, size: 19, color: '1A1915' })] }),
            ]))) })] : []),
          ],
        }) as any);
        y = mm2tw(cv.top_mm.std) + mm2tw((cv.show_sub || hasIcons ? 8.5 : 22) + (hasIcons ? 14 : 0) + (cv.show_sub ? subRowMm : 0));
      }
      at(140, 2, 40, [this.run('THIRD-PARTY INTEGRATED MANAGEMENT SYSTEM AUDIT REPORT', { bold: true, size: 40, color: '181717' })], { indent: 900 });
      at(cv.top_mm.rn, 1, 20, [this.run(rnText, { size: 20, color: cv.rn_color })]);
      // bottom band of the picture runs 261–285 mm; LibreOffice prints every cover line ~4 mm below the coded value,
      // so these sit a little higher than the sample to keep U.A.E and qrsyst.com inside the band (2026-11-05)
      at(263, 1, 24, [this.run('QUALITY REGISTRAR SYSTEMS', { bold: true, size: 24, color: 'FFFEFD' })]);
      at(269, 1, 20, [this.run('U.A.E', { size: 20, color: 'FFFEFD' })]);
      at(274, 1, 13, [this.run('qrsyst.com', { size: 13, color: 'FFFEFD' })]);
    } else if (cv.top_mm) {
      // fixed positions measured from the sample Word file: each paragraph gets an exact line height, so the next one can be
      // placed at an absolute distance from the top of the page (top margin of the cover section = 900 twips)
      let y = 900;
      const at = (mm: number, lines: number, sizeHalfPt: number, kids: TextRun[], align = AlignmentType.CENTER) => {
        const line = Math.round((sizeHalfPt / 2) * 1.25 * 20);
        const before = Math.max(0, mm2tw(mm) - y);
        coverKids.push(new Paragraph({ alignment: align, indent: { left: 700, right: 700 }, spacing: { before, after: 0, line, lineRule: 'exact' as any }, children: kids }));
        y = mm2tw(mm) + lines * line;
      };
      if (cv.label) at(cv.top_mm.label, 1, 42, [this.run(cv.label, { bold: true, size: 42, color: cv.label_color })]);
      const coSize = company.length > 40 ? 32 : company.length > 26 ? 38 : 44;
      at(cv.top_mm.company, company.length > 26 ? 2 : 1, coSize, [this.run(company, { bold: true, size: coSize, color: cv.company_color })]);
      if (c.multi) {
        const names = (c.standards ?? []).map((x: any) => String(x.main ?? '')).join(' / ');
        at(cv.top_mm.std, 1, 48, [this.run(names, { bold: true, size: 48, color: cv.std_color })]);
      } else {
        const stdMain = String(c.standard_main ?? '');
        const sp = stdMain.indexOf(' ');
        const stdRuns = cv.std_color2 && sp > 0
          ? [this.run(stdMain.slice(0, sp), { bold: true, size: 64, color: cv.std_color }), this.run(stdMain.slice(sp), { bold: true, size: 64, color: cv.std_color2 })]
          : [this.run(stdMain, { bold: true, size: 64, color: cv.std_color })];
        at(cv.top_mm.std, 1, 64, stdRuns);
        if (cv.show_sub && c.standard_sub) at(cv.top_mm.std + 13, 1, 36, [this.run(String(c.standard_sub).toUpperCase(), { size: 36, color: cv.sub_color })]);
      }
      at(cv.top_mm.rn, 1, 20, [this.run(rnText, { size: 20, color: cv.rn_color })]);
    } else {
      coverKids.push(
        new Paragraph({
          spacing: { before: 4300, after: 0, line: 300, lineRule: 'atLeast' as any },
          indent: { left: 900, right: 900 },
          alignment: AlignmentType.CENTER,
          children: [this.run(company, { bold: true, size: company.length > 40 ? 38 : company.length > 26 ? 44 : 50, color: cv.company_color })],
        }),
      );
      if (c.multi) {
        (c.standards ?? []).forEach((s: any) => {
          coverKids.push(new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 200 }, children: [this.run(s.main, { bold: true, size: 48, color: cv.std_color })] }));
          if (s.sub && cv.show_sub) coverKids.push(new Paragraph({ alignment: AlignmentType.CENTER, children: [this.run(String(s.sub).toUpperCase(), { size: 26, color: cv.sub_color })] }));
        });
      } else {
        coverKids.push(new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 500 }, children: [this.run(c.standard_main ?? '', { bold: true, size: 68, color: cv.std_color })] }));
        if (c.standard_sub && cv.show_sub) coverKids.push(new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 100 }, children: [this.run(String(c.standard_sub).toUpperCase(), { size: 40, color: cv.sub_color })] }));
      }
      coverKids.push(new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 300 }, children: [this.run(rnText, { size: 22, color: cv.rn_color })] }));
    }

    // ── body ──
    const body = this.buildBody(vm);

    // header / footer for body pages
    const headerKids: Paragraph[] = [];
    if (crest || stripe) {
      const runs: ImageRun[] = [];
      if (stripe) {
        runs.push(new ImageRun({
          type: 'jpg', data: stripe, transformation: { width: px(210), height: px(4.2) },
          floating: {
            horizontalPosition: { relative: HorizontalPositionRelativeFrom.PAGE, offset: 0 },
            verticalPosition: { relative: VerticalPositionRelativeFrom.PAGE, offset: px(14.2) * 9525 },
            behindDocument: true, wrap: { type: TextWrappingType.NONE },
          },
        }));
      }
      if (crest) {
        const d = imageSize(crest);
        const hh = px(19.4);
        runs.push(new ImageRun({
          type: 'png', data: crest, transformation: { width: Math.round((hh * d.w) / d.h), height: hh },
          floating: {
            horizontalPosition: { relative: HorizontalPositionRelativeFrom.PAGE, offset: px(14.8) * 9525 },
            verticalPosition: { relative: VerticalPositionRelativeFrom.PAGE, offset: px(4.8) * 9525 },
            wrap: { type: TextWrappingType.NONE },
          },
        }));
      }
      headerKids.push(new Paragraph({ children: runs }));
    } else headerKids.push(new Paragraph({ children: [] }));

    const footer = new Footer({
      children: [
        new Paragraph({
          alignment: AlignmentType.CENTER,
          children: [
            this.run('Page ', { size: 16, color: '555555' }),
            new TextRun({ children: [PageNumber.CURRENT], size: 16, color: '555555', font: FONT }),
            this.run(' of ', { size: 16, color: '555555' }),
            new TextRun({ children: [PageNumber.TOTAL_PAGES], size: 16, color: '555555', font: FONT }),
            this.run(`${vm.show_watermark || vm.meta?.draft ? '  —  DRAFT (not yet approved)' : ''}  —  This document is uncontrolled once printed/downloaded.`, { size: 16, color: '555555' }),
          ],
        }),
      ],
    });

    const pageA4 = { size: { width: 11906, height: 16838 } };
    const doc = new Document({
      creator: process.env.AUDIT_REPORT_PDF_AUTHOR || 'Quality Registrar Systems',
      title: `Audit Report ${vm.meta?.report_no ?? ''}`.trim(),
      styles: { default: { document: { run: { font: FONT, size: SZ } } } },
      numbering: {
        config: [{
          reference: 'ev-bullet',
          levels: [{ level: 0, format: LevelFormat.BULLET, text: '•', alignment: AlignmentType.LEFT,
            style: { paragraph: { indent: { left: 250, hanging: 200 } }, run: { font: FONT } } }],
        }],
      },
      sections: [
        { properties: { page: { ...pageA4, margin: { top: 900, bottom: 300, left: 900, right: 900 } } }, children: coverKids }, // small bottom margin: the IMS cover prints text in the bottom band
        {
          properties: { page: { ...pageA4, margin: { top: 1900, bottom: 1000, left: 900, right: 900, header: 300, footer: 500 } } },
          headers: { default: new Header({ children: headerKids }) },
          footers: { default: footer },
          children: body,
        },
      ],
    });
    return Packer.toBuffer(doc);
  }

  // ───────────────────────── body sections (mirror form-04-2a.hbs) ─────────────────────────
  private buildBody(vm: any): (Paragraph | Table)[] {
    const out: (Paragraph | Table)[] = [];
    const m = vm.meta ?? {};
    const n = vm.n ?? {};
    const org = vm.org ?? {};
    const c = vm.cover ?? {};
    const stage1 = !!m.is_stage1;
    // per-standard wording (Form 04-2a / 04-2b). Old snapshots carry no labels: resolve them from the standard name.
    const L = vm.labels ?? profileFor(String(m.standard_name ?? '')).labels;

    // ── first page (positions measured from the Word form) ──
    const sp = (pt: number) => Math.round(pt * 20);
    const line = (text: string, o: { size?: number; bold?: boolean; align?: any; before?: number; after?: number; box?: boolean | null } = {}) => {
      const kids: TextRun[] = [this.run(text, { bold: o.bold, size: o.size ?? 20 })];
      if (o.box !== undefined && o.box !== null) kids.push(this.run(' '), this.box(o.box));
      return new Paragraph({ alignment: o.align ?? AlignmentType.LEFT, spacing: { before: sp(o.before ?? 0), after: sp(o.after ?? 0), line: 244, lineRule: 'atLeast' as any }, children: kids });
    };
    out.push(new Paragraph({ spacing: { before: 0, after: 0, line: 240, lineRule: 'exact' as any }, children: [this.run('', { size: 2 })] }));
    out.push(new Paragraph({ spacing: { before: 0, after: 300 }, children: [this.run('', { size: 2 })] }));
    out.push(line('THIRD PARTY MANAGEMENT SYSTEM AUDIT REPORT', { size: 36, bold: true, align: AlignmentType.CENTER, before: 40 }));
    out.push(new Paragraph({ spacing: { before: 0, after: 0, line: 240, lineRule: 'exact' as any }, children: [this.run('', { size: 2 })] }));
    out.push(new Paragraph({ spacing: { before: 0, after: 400 }, children: [this.run('', { size: 2 })] }));
    out.push(line('OF', { size: 36, bold: true, align: AlignmentType.CENTER, before: 50 }));
    // company name: wraps onto centred lines inside an indented block; long names drop to a smaller size
    const coName = String(c.company ?? '').toUpperCase();
    const coSize = coName.length > 40 ? 26 : coName.length > 26 ? 30 : 36;
    out.push(new Paragraph({
      alignment: AlignmentType.CENTER,
      spacing: { before: sp(34), after: 0, line: Math.round((coSize / 2) * 1.2 * 20), lineRule: 'atLeast' as any },
      indent: { left: 700, right: 700 },
      children: [this.run(coName, { bold: true, size: coSize })],
    }));
    out.push(line('Accreditation Body:', { bold: true, align: AlignmentType.RIGHT, before: 14 }));
    out.push(line('ASCB', { align: AlignmentType.RIGHT, before: 9, box: !!c.accreditation_ascb }));
    out.push(line('Others' + (c.accreditation_other ? ` (${c.accreditation})` : ''), { align: AlignmentType.RIGHT, before: 13, box: !!c.accreditation_other }));
    out.push(line('Reviewed &', { bold: true, before: 4 }));
    out.push(line('Approved by Scheme Dept:', { bold: true }));
    out.push(new Paragraph({ alignment: AlignmentType.JUSTIFIED, spacing: { before: sp(62), after: 0, line: 244, lineRule: 'exact' as any }, children: [this.run(String(vm.confidentiality ?? ''), { bold: true, size: 20 })] }));

    out.push(new Paragraph({
      spacing: { before: sp(97), after: 100 }, keepNext: true, indent: { left: -108 },
      shading: { type: ShadingType.CLEAR, color: 'auto', fill: this.T.primary },
      children: [this.run('CONCLUSION & RECOMMENDATION', { bold: true, color: this.T.primary_text, size: 21 })],
    }));
    out.push(this.table([760, W - 760], (vm.conclusion_options ?? []).map((o: any) =>
      this.row([{ w: 760, center: true, vMiddle: true, children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [this.box(!!o.on)] })] }, { text: o.text, w: W - 760, vMiddle: true }], { minH: 436 }))));

    // the document box and the audit-type tick boxes start the next page (as in the Word form)
    out.push(new Paragraph({ pageBreakBefore: true, spacing: { before: 0, after: 0 }, children: [this.run('', { size: 2 })] }));
    out.push(this.table([6200, W - 6200], [this.row([
      { w: 6200, children: [this.p('QUALITY REGISTRAR SYSTEM', { bold: true, color: this.T.brand, size: 26, after: 20 }), this.p(String(m.docbox_name || m.standard_name || ''), { color: this.T.docbox_text })] },
      { w: W - 6200, children: [this.p(`Doc. No.: ${m.form_no ?? ''}`, { size: 16, after: 0, color: this.T.docbox_text }), this.p(`Issue: ${m.issue ?? ''}   Rev: ${m.rev ?? ''}`, { size: 16, after: 0, color: this.T.docbox_text }), this.p(`Date: ${m.effective_date ?? ''}`, { size: 16, color: this.T.docbox_text })] },
    ])]));
    out.push(this.p(m.report_title || `${m.standard_name ?? ''} AUDIT REPORT`, { bold: true, center: true, size: 30, before: 160, color: this.T.page_title }));
    if (m.report_title_line2) out.push(this.p(String(m.report_title_line2), { bold: true, center: true, size: 22, color: this.T.page_title }));
    out.push(this.p(m.form_subtitle || 'Stage 1 / Stage 2 / Surveillance / Recertification — Checklist and Report', { italics: true, center: true }));
    out.push(this.opts(m.audit_type_boxes ?? []));

    // ── 1. organization ──
    const l = [2822, 3977, 1701, 1649];
    out.push(this.bar(`${n.org}. Organization details`));
    out.push(this.table(l, [
      this.row([this.lbl('Name of Organization', l[0]), { text: org.name, w: l[1] }, this.lbl('Report No.', l[2]), { text: m.report_no, w: l[3] }]),
      this.row([this.lbl('Address', l[0]), { text: org.address, w: l[1] }, this.lbl('Certificate No.', l[2]), { text: org.cert_no, w: l[3] }]),
      this.row([this.lbl('Site Address (if any)', l[0]), { text: org.site_address, w: l[1] }, this.lbl('NACE/EAC Code', l[2]), { text: org.ea_code, w: l[3] }]),
      this.row([this.lbl('No. of Employees', l[0]), { text: org.employees, w: W - l[0], span: 3 }]),
      this.row([this.lbl('E-mail ID', l[0]), { text: org.emails, w: W - l[0], span: 3 }]),
      this.row([this.lbl('Contact Person', l[0]), { text: org.contact_person, w: W - l[0], span: 3 }]),
      this.row([this.lbl('Telephone / Fax', l[0]), { text: org.telephone, w: W - l[0], span: 3 }]),
      this.row([this.lbl('Multisite', l[0]), { w: W - l[0], span: 3, children: [this.opts([{ text: 'Yes', on: !!org.multisite_yes }, { text: 'No', on: !!org.multisite_no }])] }]),
      this.row([this.lbl('Scope of Audit', l[0]), { text: org.scope, w: W - l[0], span: 3 }]),
    ]));

    // ── 2. team ──
    const t2 = [2811, 7295];
    out.push(this.bar(`${n.team}. Audit team & logistics`));
    out.push(this.table(t2, [
      ['Lead Auditor', org.lead_auditor], ['Auditor(s)', org.auditors], ['Technical Expert', org.technical_expert],
      ['Audit Man-day(s)', org.man_days], ['Audit Date', org.audit_date],
    ].map(([a, b]) => this.row([this.lbl(a as string, t2[0]), { text: b as string, w: t2[1] }], { minH: 420 }))));
    out.push(this.spacer(80));
    out.push(this.table(t2, [
      this.row([this.lbl('Audit Objective / Purpose', t2[0]), { w: t2[1], children: this.sectionParas(vm, 'objective', { justify: true }) }]),
      this.row([this.lbl('Audited Criteria', t2[0]), { w: t2[1], children: this.sectionParas(vm, 'criteria', { justify: true }) }]),
    ]));

    // ── 3. change detail ──
    const t3 = [5784, 4322];
    out.push(this.bar(`${n.change}. Change detail`));
    out.push(this.table(t3, (vm.change_detail ?? []).map((q: any) => this.row([
      this.lbl(q.label, t3[0]),
      { w: t3[1], children: [q.yesno
        ? this.opts([{ text: 'Yes', on: !!q.yes }, { text: 'No', on: !!q.no }])
        : this.opts([{ text: 'None', on: !!q.none }, { text: `Yes (specify): ${q.note ?? ''}`, on: !!q.yes }])] },
    ]))));

    // ── 4. categories / 5. disclaimer ──
    const t4 = [1838, 2054, 6214];
    out.push(this.bar(`${n.cats}. Categories of findings`));
    out.push(this.table(t4, [
      this.headRow(['Category', 'Classification', 'Definition'], t4),
      ...(vm.categories ?? []).map((k: any) => this.row([{ text: k.category, w: t4[0] }, { text: k.classification, w: t4[1], bold: true }, { text: k.definition, w: t4[2] }])),
    ]));
    out.push(this.bar(`${n.disc}. Disclaimer`));
    out.push(this.table([W], [this.row([{ text: vm.disclaimer, w: W }])]));

    // ── 6. brief / 7. attendance ──
    out.push(this.bar(`${n.brief}. Brief about the organization`));
    out.push(this.table([W], [this.row([{ w: W, children: [...this.sectionParas(vm, 'org_brief', { justify: true }), ...this.gallery(vm.images?.org_brief, { perRow: 3, cellTwips: 3300, heightMm: 40 })] }])]));
    const t7 = [3636, 3255, 3215];
    out.push(this.bar(`${n.attend}. Attendance sheet`));
    out.push(this.table(t7, [
      this.headRow(['Name of Person', 'Designation', 'Signature'], t7),
      ...(vm.attendance ?? []).map((a: any) => this.row([{ text: a.name, w: t7[0] }, { text: a.designation, w: t7[1] }, { text: a.signature_path ? 'signed' : '', w: t7[2] }], { minH: 432 })),
    ]));

    // ── 8. audit brief ──
    out.push(this.bar(`${n.auditBrief}. Audit brief and facts`));
    out.push(this.table([W], [this.row([{ w: W, children: [...this.sectionParas(vm, 'audit_brief', { justify: true }), ...this.gallery(vm.images?.audit_brief, { perRow: 3, cellTwips: 3300, heightMm: 40 })] }])]));

    // ── 9. clause findings ──
    const cw = [900, 2600, 1300, 5306];
    const fullW = cw[0] + cw[1] + cw[2] + cw[3];
    out.push(this.bar(`${n.checklist}. ${stage1 ? 'Document review & readiness checklist' : 'Audit findings by clause'}`));
    (vm.clause_groups ?? []).forEach((g: any) => {
      out.push(this.groupHeading(g.heading));
      out.push(this.table(cw, [
        this.headRow(['Clause', 'Requirement', 'Status', 'Objective Evidence / Audit Comments'], cw),
        ...(g.rows ?? []).flatMap((r: any): TableRow[] => {
          const main = this.row([
            { text: r.label, w: cw[0], bold: true, center: true, vMiddle: true },
            { text: r.requirement, w: cw[1], vMiddle: true },
            this.statusCell(r, cw[2]),
            this.evidenceTextCell(r, cw[3]),
          ], { minH: 400 });
          const photo = this.photoRowFor(r, fullW, 4);
          return photo ? [main, photo] : [main];
        }),
      ]));
    });

    if (!stage1) {
      const pw = [2266, 1294, 6546];
      const pwFull = pw[0] + pw[1] + pw[2];
      // Performance / risk / feedback / statutory tables: same layout as section 9 — text in the row, photos in a shaded row beneath
      const perfTable = (labelHead: string, rows: any[], minH = 400) => this.table(pw, [
        this.headRow([labelHead, 'Status', 'Objective Evidence Reviewed & Audit Finding'], pw),
        ...rows.flatMap((r: any): TableRow[] => {
          const main = this.row([
            { text: r.label, w: pw[0], bold: true, vMiddle: true },
            this.statusCell(r, pw[1]),
            this.evidenceTextCell(r, pw[2]),
          ], { minH });
          const photo = this.photoRowFor(r, pwFull, 3);
          return photo ? [main, photo] : [main];
        }),
      ]);
      out.push(this.bar(`${n.perf}. ${L.perf_title}`));
      (vm.perf_groups ?? []).forEach((g: any) => out.push(perfTable(L.perf_col, g.rows ?? []), this.spacer(80)));
      // interview line of the Word form (who was interviewed + awareness statement)
      out.push(this.table([W], [
        this.row([{ w: W, children: [new Paragraph({ spacing: { after: 40 }, children: [this.run('Interviewed with (tick / specify):  ', { bold: true }), ...(L.interview_roles ?? []).flatMap((r: string, i: number) => [this.run(i ? '   ' : ''), this.box(false), this.run(' ' + r)])] })] }]),
        this.row([{ text: L.interview_text ?? '', w: W }]),
      ]));
      out.push(this.spacer(80));
      out.push(this.bar(`${n.risk}. ${L.risk_title || 'Risk management'}`));
      out.push(perfTable('Element', vm.risk_rows ?? []));
      out.push(this.bar(`${n.feedback}. ${L.feedback_title}`));
      out.push(perfTable('Element', vm.feedback_rows ?? []));
      out.push(this.bar(`${n.statutory}. Statutory / regulatory compliance`));
      out.push(perfTable('Requirement Type', vm.statutory_rows ?? []));

      out.push(this.bar(`${n.mark}. Use of certification mark / logo`));
      out.push(this.table([500, W - 500], (vm.mark_use ?? []).map((o: any, i: number, arr: any[]) => this.row([
        { w: 500, center: true, children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [this.box(!!o.on)] })] },
        { text: o.text + (i === arr.length - 1 && vm.mark_use_nc_no ? ' ' + vm.mark_use_nc_no : ''), w: W - 500 },
      ]))));

      out.push(this.bar(`${n.effectiveness}. Auditor's overall conclusion`));
      out.push(this.table([W], [this.row([{ w: W, children: this.richParas(vm.sections_rich?.effectiveness) }])]));
    }

    // ── summary of audit ──
    out.push(this.bar(`${n.summary}. Summary of audit`));
    if (!stage1) {
      out.push(this.table([W], [
        this.row([{ text: 'Non-conformities and Corrective Action in Previous Assessment (Stage 1, Stage 2, Surveillance & Re-assessment)', w: W, fill: this.T.primary, bold: true, color: this.T.primary_text }]),
        this.row([{ w: W, children: this.sectionParas(vm, 'prev_nc', { justify: true }) }]),
      ]));
      out.push(this.spacer(80));
    }
    const nw = [606, 2222, 1415, 5863];
    const ncRows: any[] = vm.nc_rows ?? [];
    out.push(this.table(nw, [
      this.headRow(['No.', 'Clause', 'Type', 'Area / Description of Finding'], nw),
      ...(ncRows.length
        ? ncRows.map((r) => this.row([{ text: String(r.no), w: nw[0], center: true }, { text: r.clause, w: nw[1] }, { text: r.type, w: nw[2] }, { w: nw[3], children: this.paras(r.statement, { justify: true }) }]))
        : [this.row([{ text: 'No non-conformities or observations were recorded for this audit.', w: W, span: 4, center: true }])]),
    ]));
    if (ncRows.length || vm.nc_counts_fixed) {
      const z = (v: any) => (Number(v) ? String(v) : '-');
      out.push(this.spacer(80));
      out.push(new Paragraph({ keepNext: true, spacing: { after: 60 }, children: [this.run('Category of Audit Findings', { bold: true })] }));
      const cwid = [542, 3849, 1897, 1897, 1921];
      out.push(this.table(cwid, [
        this.headRow(['#', 'Component (Clause)', 'No. of Major NC', 'No. of Minor NC', 'No. of Observations'], cwid),
        ...(vm.nc_counts ?? []).map((r: any) => this.row([{ text: String(r.no), w: cwid[0], center: true }, { text: r.component, w: cwid[1] }, { text: z(r.major), w: cwid[2], center: true }, { text: z(r.minor), w: cwid[3], center: true }, { text: z(r.obs), w: cwid[4], center: true }])),
        this.row([{ text: '', w: cwid[0] }, { text: 'Total', w: cwid[1], bold: true }, { text: z(vm.nc_totals?.major), w: cwid[2], bold: true, center: true }, { text: z(vm.nc_totals?.minor), w: cwid[3], bold: true, center: true }, { text: z(vm.nc_totals?.obs), w: cwid[4], bold: true, center: true }]),
      ]));
    }

    if (!stage1) {
      out.push(this.spacer(80));
      const iw = [4300, W - 4300];
      out.push(this.table(iw, [
        this.row([{ text: 'Recommendation for Continual Improvement', w: iw[0], fill: this.T.primary, bold: true, color: this.T.primary_text }, { text: 'Audit Team Comments', w: iw[1], fill: this.T.primary, bold: true, color: this.T.primary_text }]),
        this.row([
          { w: iw[0], children: [this.opts([{ text: 'Modification/update required to Management System', on: !!vm.improvement?.modification_required }]), this.opts([{ text: 'No further action required at this time', on: !!vm.improvement?.no_action }])] },
          { w: iw[1], children: this.sectionParas(vm, 'improvement_comments', { justify: true }) },
        ]),
      ]));
    }

    // ── declaration ──
    out.push(this.bar(`${n.decl}. Team leader declaration`));
    out.push(new Paragraph({ keepNext: true, spacing: { after: 100 }, children: [this.run('Tick each statement as applicable.', { italics: true, size: 16 })] }));
    // keepNext on every paragraph keeps the declaration block together with the date table (no orphan last page)
    const tick = (list: any[]) => this.table([755, W - 755], (list ?? []).map((o: any) => o.heading
      ? this.row([{ w: W, span: 2, fill: this.T.primary, bold: true, color: this.T.primary_text, children: [new Paragraph({ keepNext: true, children: [this.run(String(o.text ?? ''), { bold: true, color: this.T.primary_text, size: 16 })] })] }], { cantSplit: true })
      : this.row([
      { w: 755, center: true, vMiddle: true, children: [new Paragraph({ keepNext: true, alignment: AlignmentType.CENTER, children: [this.box(!!o.on)] })] },
      { w: W - 755, vMiddle: true, children: [new Paragraph({ keepNext: true, spacing: { after: 40 }, children: [this.run(String(o.text ?? ''), { size: 16 })] })] },
    ], { minH: 454, cantSplit: true })));
    out.push(tick(vm.declarations));
    if (!stage1) {
      out.push(new Paragraph({ keepNext: true, spacing: { before: 160, after: 100 }, children: [this.run('The following documents were also reviewed during the audit.', { italics: true, size: 16 })] }));
      out.push(tick(vm.attachments));
    }

    if (stage1) {
      out.push(this.bar(`${n.rec}. Recommendation`));
      out.push(tick(vm.recommendation_options));
      out.push(this.spacer(80));
      out.push(this.table([5779, W - 5779], [this.row([this.lbl('Proposed date for Stage 2 Audit', 5779), { text: vm.next_audit_date || '__ __ / __ __ / __ __ __ __  (dd/mm/yyyy)', w: W - 5779 }])]));
    } else {
      out.push(new Paragraph({ keepNext: true, spacing: { before: 280, after: 0 }, children: [] }));
      out.push(this.table([5779, W - 5779], [this.row([this.lbl('Proposed date for next audit (Surveillance / Recertification)', 5779), { text: vm.next_audit_date || '__ __ / __ __ / __ __ __ __  (dd/mm/yyyy)', w: W - 5779 }])]));
    }

    void PageBreak;
    return out;
  }
}