#!/usr/bin/env node
/*
 * Deeper Signals Booklet generator.
 *
 * Reads a JSON content file and produces a branded .docx that matches the
 * Deeper Signals Booklet template (cover page + body styles).
 *
 * Usage:
 *   node generate_booklet.js <content.json> <output.docx> [assets_dir]
 *
 * Asset directory must contain logo.png and cover_gradient.png; defaults
 * to ../assets relative to this script.
 *
 * Block types supported in content.body:
 *   title, h1, h2, h3, h4, paragraph/body, spacer, page_break,
 *   bullets, emphasis_list, callout1, callout2, table
 *
 * Compactness and pagination:
 *   - Default line spacing is tightened to ~1.1 and paragraph spacing reduced,
 *     so the document stays compact (target: cover + 3 content pages).
 *   - Every heading carries keepNext so a heading never lands alone at the
 *     bottom of a page (the body that follows is pulled with it).
 *   - Table rows carry cantSplit so a row never breaks across a page.
 */

const fs = require('fs');
const path = require('path');
const {
  Document, Packer, Paragraph, TextRun, ImageRun, Table, TableRow, TableCell,
  AlignmentType, BorderStyle, WidthType, ShadingType, HeadingLevel, LevelFormat,
  PageBreak, Footer, Header, PageNumber, HorizontalPositionRelativeFrom,
  VerticalPositionRelativeFrom, TextWrappingType, TextWrappingSide
} = require('docx');

// ---------------- Brand tokens ----------------
const COLORS = {
  pink: 'FF005C',           // Heading 1 / Title accent
  pinkBar: 'F9DCDF',        // Callout left bar
  pinkBg: 'FCF4F5',         // Callout 1 background / emphasis row fill
  ink: '0D0D0D',            // Cover title and subheading color
  black: '000000',
  muted: '666666',          // Heading 4 color
  white: 'FFFFFF',
  rule: 'E6E6E6',           // light table row separator
};

const FONTS = {
  bodyMedium: 'Manrope Medium',
  body: 'Manrope',
  bodySemiBold: 'Manrope SemiBold',
  display: 'Space Grotesk Medium',
  displaySemi: 'Space Grotesk SemiBold',
  bullet: 'Roboto',
};

// docx size is in half-points: 24 = 12pt
const SIZES = {
  body: 21,             // 10.5pt
  small: 19,            // ~9.5pt (table cells, methodology)
  coverTitle: 112,      // 56pt
  coverSubheading: 54,  // 27pt
  coverTagline: 28,     // 14pt
  title: 36,            // 18pt
  heading1: 36,         // 18pt
  heading2: 27,         // ~13.5pt
  heading3: 23,         // ~11.5pt
  heading4: 22,         // 11pt
};

// Page geometry (DXA: 1440 = 1 inch). US Letter portrait.
const PAGE = {
  width: 12240,
  height: 15840,
  marginTop: 1260,
  marginBottom: 1080,
  marginLeft: 1440,
  marginRight: 1440,
  header: 720,
  footer: 600,
};

const CONTENT_WIDTH = PAGE.width - PAGE.marginLeft - PAGE.marginRight; // 9360

// Compact spacing defaults
const LINE = 264;         // ~1.1 line spacing
const AFTER = 120;        // 6pt after body paragraphs

// ---------------- Helpers ----------------

function loadImage(assetsDir, name) {
  const p = path.join(assetsDir, name);
  if (!fs.existsSync(p)) throw new Error(`Missing asset: ${p}`);
  return fs.readFileSync(p);
}

function inchesToPx(inches) {
  return Math.round(inches * 96);
}

// Run helper to keep call sites short
function run(text, opts = {}) {
  const rp = {
    font: opts.font || FONTS.bodyMedium,
    size: opts.size || SIZES.body,
  };
  if (opts.bold) rp.bold = true;
  if (opts.italics) rp.italics = true;
  if (opts.color) rp.color = opts.color;
  rp.text = text;
  return new TextRun(rp);
}

// Body paragraph from a mixed-runs spec or a plain string
function bodyPara(content, opts = {}) {
  const runs = Array.isArray(content)
    ? content.map(r => (r instanceof TextRun ? r : run(r.text, r)))
    : [run(content)];
  return new Paragraph({
    children: runs,
    spacing: { after: opts.after != null ? opts.after : AFTER, line: LINE, lineRule: 'auto' },
    ...(opts.alignment ? { alignment: opts.alignment } : {}),
  });
}

// Blank paragraph (small vertical gap)
function spacer() {
  return new Paragraph({ spacing: { after: 0, line: 240, lineRule: 'auto' }, children: [new TextRun('')] });
}

// ---------------- Cover page ----------------

function coverPage(meta, assetsDir) {
  const children = [];

  const logoBuf = loadImage(assetsDir, 'logo.png');
  children.push(new Paragraph({
    spacing: { after: 0 },
    children: [new ImageRun({
      data: logoBuf,
      transformation: { width: inchesToPx(1.3), height: inchesToPx(0.85) },
      type: 'png',
    })],
  }));

  const gradientBuf = loadImage(assetsDir, 'cover_gradient.png');
  children.push(new Paragraph({
    spacing: { after: 0 },
    children: [new ImageRun({
      data: gradientBuf,
      type: 'png',
      transformation: { width: 816, height: 1056 },
      floating: {
        horizontalPosition: { relative: HorizontalPositionRelativeFrom.PAGE, offset: 0 },
        verticalPosition: { relative: VerticalPositionRelativeFrom.PAGE, offset: 0 },
        behindDocument: true,
        wrap: { type: TextWrappingType.NONE, side: TextWrappingSide.BOTH_SIDES },
        zIndex: 1,
      },
    })],
  }));

  // Vertical spacer before the title block (positions title partway down the page).
  for (let i = 0; i < 4; i++) children.push(spacer());

  // Big title
  children.push(new Paragraph({
    spacing: { after: 0, line: 276, lineRule: 'auto' },
    alignment: AlignmentType.LEFT,
    children: [new TextRun({
      text: meta.title || 'Title',
      font: FONTS.display,
      size: SIZES.coverTitle,
      color: COLORS.ink,
    })],
  }));

  // Subheading
  children.push(new Paragraph({
    spacing: { before: 0, after: 320 },
    alignment: AlignmentType.LEFT,
    children: [new TextRun({
      text: meta.subheading || '',
      font: FONTS.display,
      size: SIZES.coverSubheading,
      color: COLORS.ink,
    })],
  }));

  children.push(spacer());

  // Tagline: line 1 (dark, medium), line 2 (pink, semibold)
  const taglineLine1 = meta.taglineLine1 || 'Finding and growing top talent is hard.';
  const taglineLine2 = meta.taglineLine2 || 'Deeper Signals makes it easy.';
  children.push(new Paragraph({
    spacing: { after: 0 },
    children: [new TextRun({
      text: taglineLine1, font: FONTS.bodyMedium, size: SIZES.coverTagline, color: COLORS.black,
    })],
  }));
  children.push(new Paragraph({
    spacing: { after: 0 },
    children: [new TextRun({
      text: taglineLine2, font: FONTS.bodySemiBold, size: SIZES.coverTagline, color: COLORS.pink,
    })],
  }));

  children.push(spacer());
  if (meta.date) {
    children.push(new Paragraph({
      spacing: { after: 0 },
      children: [new TextRun({
        text: meta.date, font: FONTS.bodyMedium, size: SIZES.body, color: COLORS.black,
      })],
    }));
  }

  // Page break to start body content on page 2
  children.push(new Paragraph({ children: [new PageBreak()] }));

  return children;
}

// ---------------- Body content elements ----------------

function titleHeading(text) {
  return new Paragraph({
    spacing: { before: 160, after: 80, line: 288, lineRule: 'auto' },
    keepNext: true,
    children: [new TextRun({ text, font: FONTS.displaySemi, size: SIZES.title, color: COLORS.pink })],
  });
}

function h1(text) {
  return new Paragraph({
    spacing: { before: 160, after: 80, line: 288, lineRule: 'auto' },
    keepNext: true,
    children: [new TextRun({ text, font: FONTS.displaySemi, size: SIZES.heading1, color: COLORS.pink })],
  });
}

function h2(text) {
  return new Paragraph({
    spacing: { before: 140, after: 60, line: 276, lineRule: 'auto' },
    keepNext: true,
    children: [new TextRun({ text, font: FONTS.bodySemiBold, size: SIZES.heading2, color: COLORS.black })],
  });
}

function h3(text) {
  return new Paragraph({
    spacing: { before: 120, after: 50, line: 264, lineRule: 'auto' },
    keepNext: true,
    children: [new TextRun({ text, font: FONTS.bodySemiBold, size: SIZES.heading3, color: COLORS.black })],
  });
}

function h4(text) {
  return new Paragraph({
    spacing: { before: 120, after: 40, line: 264, lineRule: 'auto' },
    keepNext: true,
    children: [new TextRun({ text, font: FONTS.bodyMedium, size: SIZES.heading4, color: COLORS.muted })],
  });
}

// Emphasis list item: bold label, then body text after the colon
function emphasisItem({ label, text }) {
  return new Paragraph({
    numbering: { reference: 'ds-bullets', level: 0 },
    spacing: { after: 80, line: LINE, lineRule: 'auto' },
    indent: { left: 360, hanging: 360 },
    children: [
      new TextRun({ text: `${label}: `, font: FONTS.bodySemiBold, size: SIZES.body, bold: true }),
      new TextRun({ text, font: FONTS.body, size: SIZES.body }),
    ],
  });
}

// Plain bullet item (single text string or run-spec array)
function bulletItem(content) {
  const runs = Array.isArray(content)
    ? content.map(r => (r instanceof TextRun ? r : run(r.text, r)))
    : [run(content)];
  return new Paragraph({
    numbering: { reference: 'ds-bullets', level: 0 },
    spacing: { after: 80, line: LINE, lineRule: 'auto' },
    indent: { left: 360, hanging: 360 },
    children: runs,
  });
}

// ---------------- Callouts ----------------

// Callout 1: filled pink background, thick pink left bar, all-bold text.
function callout1(text, opts = {}) {
  const noBorder = { style: BorderStyle.NONE, size: 0, color: '000000' };
  const pinkBar = { style: BorderStyle.SINGLE, size: 24, color: COLORS.pinkBar };
  const whiteBottom = { style: BorderStyle.SINGLE, size: 24, color: COLORS.white };

  let runs;
  if (typeof text === 'string') {
    runs = [new TextRun({ text, font: FONTS.body, size: SIZES.body, bold: true })];
  } else {
    runs = text.map(r => new TextRun({
      text: r.text,
      font: r.font || FONTS.body,
      size: r.size || SIZES.body,
      bold: r.bold !== false,
      ...(r.color ? { color: r.color } : {}),
    }));
  }

  return new Table({
    width: { size: CONTENT_WIDTH, type: WidthType.DXA },
    columnWidths: [CONTENT_WIDTH],
    rows: [
      new TableRow({
        cantSplit: true,
        children: [
          new TableCell({
            width: { size: CONTENT_WIDTH, type: WidthType.DXA },
            shading: { fill: COLORS.pinkBg, type: ShadingType.CLEAR, color: 'auto' },
            borders: { top: noBorder, left: pinkBar, bottom: whiteBottom, right: noBorder },
            margins: { top: 200, bottom: 200, left: 260, right: 260 },
            children: [new Paragraph({ spacing: { after: 0, line: 288, lineRule: 'auto' }, children: runs })],
          }),
        ],
      }),
    ],
  });
}

// Callout 2: transparent background, thick pink left bar.
function callout2(paragraphs) {
  const noBorder = { style: BorderStyle.NONE, size: 0, color: '000000' };
  const pinkBar = { style: BorderStyle.SINGLE, size: 24, color: COLORS.pinkBar };
  const whiteBar = { style: BorderStyle.SINGLE, size: 24, color: COLORS.white };

  function specToRuns(spec) {
    if (typeof spec === 'string') {
      return [new TextRun({ text: spec, font: FONTS.bodyMedium, size: SIZES.body })];
    }
    if (spec.label !== undefined) {
      return [
        new TextRun({ text: `${spec.label}: `, font: FONTS.bodySemiBold, size: SIZES.body, bold: true }),
        new TextRun({ text: spec.text, font: FONTS.bodyMedium, size: SIZES.body }),
      ];
    }
    if (spec.runs) {
      return spec.runs.map(r => new TextRun({
        text: r.text,
        font: r.font || FONTS.bodyMedium,
        size: r.size || SIZES.body,
        ...(r.bold ? { bold: true } : {}),
        ...(r.italics ? { italics: true } : {}),
        ...(r.color ? { color: r.color } : {}),
      }));
    }
    return [new TextRun({ text: String(spec), font: FONTS.bodyMedium, size: SIZES.body })];
  }

  function makeCell(children, leftBorder) {
    return new TableCell({
      width: { size: CONTENT_WIDTH, type: WidthType.DXA },
      borders: { top: noBorder, left: leftBorder, bottom: noBorder, right: noBorder },
      margins: { top: 0, bottom: 0, left: 0, right: 0 },
      children,
    });
  }

  function contentRow(spec) {
    const para = new Paragraph({
      spacing: { after: 0, line: 288, lineRule: 'auto' },
      indent: { left: 270 },
      children: specToRuns(spec),
    });
    return new TableRow({ cantSplit: true, children: [makeCell([para], pinkBar)] });
  }

  function spacerRow() {
    return new TableRow({
      height: { value: 160, rule: 'atLeast' },
      children: [makeCell([new Paragraph({ spacing: { after: 0 }, children: [new TextRun('')] })], whiteBar)],
    });
  }

  const rows = [];
  paragraphs.forEach((p, idx) => {
    rows.push(contentRow(p));
    if (idx < paragraphs.length - 1) rows.push(spacerRow());
  });

  return new Table({
    width: { size: CONTENT_WIDTH, type: WidthType.DXA },
    columnWidths: [CONTENT_WIDTH],
    rows,
  });
}

// ---------------- Data table ----------------
//
// block = {
//   type: "table",
//   columns: ["", "Conservative", "Expected", "Strong"],
//   rows: [ ["Stronger performance", "$1.2m", "$1.6m", "$2.0m"], ... ],
//   col_widths: [0.40, 0.20, 0.20, 0.20],   // optional fractions; default equal
//   emphasis_rows: [2],                       // optional 0-based row indices (bold + light fill)
//   align: ["left","right","right","right"]   // optional per-column alignment
// }
function dataTable(block) {
  const cols = block.columns || [];
  const rows = block.rows || [];
  const nCols = cols.length || (rows[0] ? rows[0].length : 1);

  let widths;
  if (Array.isArray(block.col_widths) && block.col_widths.length === nCols) {
    const sum = block.col_widths.reduce((a, b) => a + b, 0) || 1;
    widths = block.col_widths.map(w => Math.round((w / sum) * CONTENT_WIDTH));
  } else {
    const w = Math.round(CONTENT_WIDTH / nCols);
    widths = Array(nCols).fill(w);
  }

  const align = block.align || cols.map((_, i) => (i === 0 ? 'left' : 'right'));
  const emphasis = new Set(block.emphasis_rows || []);
  const headerFill = block.header_fill || COLORS.pink;

  const noBorder = { style: BorderStyle.NONE, size: 0, color: '000000' };
  const ruleBorder = { style: BorderStyle.SINGLE, size: 4, color: COLORS.rule };

  function alignType(a) {
    if (a === 'right') return AlignmentType.RIGHT;
    if (a === 'center') return AlignmentType.CENTER;
    return AlignmentType.LEFT;
  }

  function headerCell(text, i) {
    return new TableCell({
      width: { size: widths[i], type: WidthType.DXA },
      shading: { fill: headerFill, type: ShadingType.CLEAR, color: 'auto' },
      borders: { top: noBorder, bottom: noBorder, left: noBorder, right: noBorder },
      margins: { top: 70, bottom: 70, left: 120, right: 120 },
      children: [new Paragraph({
        spacing: { after: 0, line: 240, lineRule: 'auto' },
        alignment: alignType(align[i]),
        children: [new TextRun({ text: String(text), font: FONTS.bodySemiBold, size: SIZES.small, bold: true, color: COLORS.white })],
      })],
    });
  }

  function bodyCell(text, i, emph) {
    return new TableCell({
      width: { size: widths[i], type: WidthType.DXA },
      shading: emph ? { fill: COLORS.pinkBg, type: ShadingType.CLEAR, color: 'auto' } : undefined,
      borders: { top: noBorder, bottom: ruleBorder, left: noBorder, right: noBorder },
      margins: { top: 64, bottom: 64, left: 120, right: 120 },
      children: [new Paragraph({
        spacing: { after: 0, line: 252, lineRule: 'auto' },
        alignment: alignType(align[i]),
        children: [new TextRun({
          text: String(text),
          font: emph ? FONTS.bodySemiBold : FONTS.bodyMedium,
          size: SIZES.small,
          bold: emph || (i === 0 && block.bold_first_col === true),
          color: COLORS.ink,
        })],
      })],
    });
  }

  const tableRows = [];
  if (cols.length) {
    tableRows.push(new TableRow({ tableHeader: true, cantSplit: true, children: cols.map((c, i) => headerCell(c, i)) }));
  }
  rows.forEach((r, ri) => {
    const emph = emphasis.has(ri);
    tableRows.push(new TableRow({ cantSplit: true, children: r.map((c, i) => bodyCell(c, i, emph)) }));
  });

  return new Table({
    width: { size: CONTENT_WIDTH, type: WidthType.DXA },
    columnWidths: widths,
    rows: tableRows,
  });
}

// ---------------- Generic block dispatcher ----------------

function buildBlock(block) {
  switch (block.type) {
    case 'title':
      return [titleHeading(block.text)];
    case 'h1':
      return [h1(block.text)];
    case 'h2':
      return [h2(block.text)];
    case 'h3':
      return [h3(block.text)];
    case 'h4':
      return [h4(block.text)];
    case 'paragraph':
    case 'body':
      return [bodyPara(block.runs || block.text, { after: block.after })];
    case 'spacer':
      return [spacer()];
    case 'page_break':
      return [new Paragraph({ children: [new PageBreak()] })];
    case 'bullets':
      return block.items.map(item => bulletItem(item));
    case 'emphasis_list':
      return block.items.map(item => emphasisItem(item));
    case 'callout1':
      return [callout1(block.runs || block.text), spacer()];
    case 'callout2':
      return [callout2(block.items || [block.text]), spacer()];
    case 'table':
      return [dataTable(block), spacer()];
    default:
      throw new Error(`Unknown block type: ${block.type}`);
  }
}

// ---------------- Document assembly ----------------

function buildDocument(content, assetsDir) {
  const coverChildren = coverPage(content.meta || {}, assetsDir);
  const bodyChildren = [];
  for (const block of (content.body || [])) {
    bodyChildren.push(...buildBlock(block));
  }

  const doc = new Document({
    creator: 'Deeper Signals',
    title: content.meta && content.meta.title ? content.meta.title : 'Deeper Signals Booklet',
    styles: {
      default: {
        document: {
          run: { font: FONTS.bodyMedium, size: SIZES.body },
          paragraph: { spacing: { line: LINE, lineRule: 'auto', after: AFTER } },
        },
      },
    },
    numbering: {
      config: [{
        reference: 'ds-bullets',
        levels: [
          {
            level: 0,
            format: LevelFormat.BULLET,
            text: '\uFF0D',
            alignment: AlignmentType.LEFT,
            style: {
              run: { font: FONTS.bullet, size: SIZES.body, color: COLORS.ink },
              paragraph: { indent: { left: 360, hanging: 360 } },
            },
          },
        ],
      }],
    },
    sections: [{
      properties: {
        page: {
          size: { width: PAGE.width, height: PAGE.height },
          margin: {
            top: PAGE.marginTop,
            right: PAGE.marginRight,
            bottom: PAGE.marginBottom,
            left: PAGE.marginLeft,
            header: PAGE.header,
            footer: PAGE.footer,
          },
          pageNumbers: { start: 1 },
        },
        titlePage: true,
      },
      headers: {
        first: new Header({ children: [new Paragraph({ children: [new TextRun('')] })] }),
      },
      footers: {
        default: new Footer({
          children: [new Paragraph({
            alignment: AlignmentType.RIGHT,
            spacing: { after: 0 },
            children: [new TextRun({
              children: [PageNumber.CURRENT],
              font: FONTS.bodyMedium, size: SIZES.body, color: COLORS.muted,
            })],
          })],
        }),
        first: new Footer({ children: [new Paragraph({ children: [new TextRun('')] })] }),
      },
      children: [...coverChildren, ...bodyChildren],
    }],
  });

  return doc;
}

// ---------------- CLI ----------------

async function main() {
  const [, , inputPath, outputPath, assetsArg] = process.argv;
  if (!inputPath || !outputPath) {
    console.error('Usage: node generate_booklet.js <content.json> <output.docx> [assets_dir]');
    process.exit(1);
  }
  const assetsDir = assetsArg ? path.resolve(assetsArg) : path.resolve(__dirname, '..', 'assets');
  const content = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
  const doc = buildDocument(content, assetsDir);
  const buffer = await Packer.toBuffer(doc);
  fs.writeFileSync(outputPath, buffer);
  console.log(`Wrote ${outputPath}`);
}

main().catch(err => { console.error(err); process.exit(1); });
