#!/usr/bin/env ts-node
/**
 * PDF Datasheet Generator - Industrial Engineering Documentation Style
 * STYLEGUIDE.md compliant: industrial, technical, restrained
 */

import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
import { PDFDocument, rgb, StandardFonts, PDFFont, PDFPage, PDFImage } from 'pdf-lib';

let sharpFn: ((input?: any, options?: any) => any) | null = null;
async function getSharp(): Promise<(input?: any, options?: any) => any> {
  if (sharpFn) return sharpFn;
  // `sharp` is CJS but this script runs as ESM via ts-node.
  // Dynamic import gives stable interop.
  const mod: any = await import('sharp');
  sharpFn = (mod?.default || mod) as (input?: any, options?: any) => any;
  return sharpFn;
}

const CONFIG = {
  productsFile: path.join(process.cwd(), 'data/processed/products.json'),
  outputDir: path.join(process.cwd(), 'public/datasheets'),
  chunkSize: 10,
  siteUrl: 'https://klz-cables.com',
};

const ASSET_MAP_FILE = path.join(process.cwd(), 'data/processed/asset-map.json');
const PUBLIC_DIR = path.join(process.cwd(), 'public');

const EXCEL_SOURCE_FILES = [
  path.join(process.cwd(), 'data/source/high-voltage.xlsx'),
  path.join(process.cwd(), 'data/source/medium-voltage-KM.xlsx'),
  path.join(process.cwd(), 'data/source/low-voltage-KM.xlsx'),
  path.join(process.cwd(), 'data/source/solar-cables.xlsx'),
];

type AssetMap = Record<string, string>;

function readAssetMap(): AssetMap {
  try {
    if (!fs.existsSync(ASSET_MAP_FILE)) return {};
    return JSON.parse(fs.readFileSync(ASSET_MAP_FILE, 'utf8')) as AssetMap;
  } catch {
    return {};
  }
}

const ASSET_MAP: AssetMap = readAssetMap();

interface ProductData {
  id: number;
  name: string;
  shortDescriptionHtml: string;
  descriptionHtml: string;
  images: string[];
  featuredImage: string | null;
  sku: string;
  slug?: string;
  path?: string;
  translationKey?: string;
  locale?: 'en' | 'de';
  categories: Array<{ name: string }>;
  attributes: Array<{
    name: string;
    options: string[];
  }>;
}

type ExcelRow = Record<string, any>;
type ExcelMatch = { rows: ExcelRow[]; units: Record<string, string> };
let EXCEL_INDEX: Map<string, ExcelMatch> | null = null;

type KeyValueItem = { label: string; value: string; unit?: string };
type VoltageTableModel = {
  voltageLabel: string;
  metaItems: KeyValueItem[];
  crossSections: string[];
  columns: Array<{ key: string; label: string; get: (rowIndex: number) => string }>;
};

function estimateDenseMetaGridHeight(itemsCount: number): number {
  // Must stay in sync with the layout constants in `drawDenseMetaGrid()`.
  const cols = itemsCount >= 7 ? 3 : 2;
  const cellH = 34;
  const titleH = 18;
  const headerPadY = 10;
  const rows = Math.ceil(Math.max(0, itemsCount) / cols);
  const boxH = headerPadY + titleH + rows * cellH + headerPadY;
  // `drawDenseMetaGrid()` returns the cursor below the box with additional spacing.
  return boxH + 18;
}

function normalizeUnit(unitRaw: string): string {
  const u = normalizeValue(unitRaw);
  if (!u) return '';
  // Temperature units: show °C (not plain C).
  if (/^c$/i.test(u) || /^°c$/i.test(u)) return '°C';
  // Common WinAnsi-safe normalizations.
  return u
    .replace(/Ω/gi, 'Ohm')
    .replace(/[\u00B5\u03BC]/g, 'u');
}

function denseAbbrevLabel(args: {
  key: string;
  locale: 'en' | 'de';
  unit?: string;
  withUnit?: boolean;
}): string {
  const u = normalizeUnit(args.unit || '');
  const withUnit = args.withUnit ?? true;
  const unitSafe = u
    .replace(/Ω/gi, 'Ohm')
    .replace(/[\u00B5\u03BC]/g, 'u');
  const suffix = withUnit && unitSafe ? ` [${unitSafe}]` : '';

  switch (args.key) {
    case 'DI':
    case 'RI':
    case 'Wi':
    case 'Ibl':
    case 'Ibe':
    case 'Wm':
    case 'Rbv':
    case 'Fzv':
    case 'G':
      return `${args.key}${suffix}`;
    case 'Ik_cond':
      return `Ik${suffix}`;
    case 'Ik_screen':
      return `Ik_s${suffix}`;
    case 'Ø':
      return `Ø${suffix}`;
    case 'Cond':
      return args.locale === 'de' ? 'Leiter' : 'Cond.';
    case 'shape':
      return args.locale === 'de' ? 'Form' : 'Shape';
    // Electrical
    case 'cap':
      return `C${suffix}`;
    case 'X':
      return `X${suffix}`;
    // Temperatures
    case 'temp_range':
      return `T${suffix}`;
    case 'max_op_temp':
      return `T_op${suffix}`;
    case 'max_sc_temp':
      return `T_sc${suffix}`;
    case 'min_store_temp':
      return `T_st${suffix}`;
    case 'min_lay_temp':
      return `T_lay${suffix}`;
    // Compliance
    case 'cpr':
      return `CPR${suffix}`;
    case 'flame':
      return `FR${suffix}`;
    // Voltages
    case 'test_volt':
      return `U_test${suffix}`;
    case 'rated_volt':
      return `U0/U${suffix}`;
    default:
      return args.key || '';
  }
}

function metaFullLabel(args: { key: string; excelKey: string; locale: 'en' | 'de' }): string {
  const key = normalizeValue(args.key);
  if (args.locale === 'de') {
    switch (key) {
      case 'test_volt':
        return 'Prüfspannung';
      case 'temp_range':
        return 'Temperaturbereich';
      case 'max_op_temp':
        return 'Leitertemperatur (max.)';
      case 'max_sc_temp':
        return 'Kurzschlusstemperatur (max.)';
      case 'min_lay_temp':
        return 'Minimale Verlegetemperatur';
      case 'min_store_temp':
        return 'Minimale Lagertemperatur';
      case 'cpr':
        return 'CPR-Klasse';
      case 'flame':
        return 'Flammhemmend';
      default:
        return formatExcelHeaderLabel(args.excelKey);
    }
  }

  // EN
  switch (key) {
    case 'test_volt':
      return 'Test voltage';
    case 'temp_range':
      return 'Operating temperature range';
    case 'max_op_temp':
      return 'Conductor temperature (max.)';
    case 'max_sc_temp':
      return 'Short-circuit temperature (max.)';
    case 'min_lay_temp':
      return 'Minimum laying temperature';
    case 'min_store_temp':
      return 'Minimum storage temperature';
    case 'cpr':
      return 'CPR class';
    case 'flame':
      return 'Flame retardant';
    default:
      return formatExcelHeaderLabel(args.excelKey);
  }
}

function expandMetaLabel(label: string, locale: 'en' | 'de'): string {
  const l = normalizeValue(label);
  if (!l) return '';

  // Safety net: the voltage-group meta grid must never show abbreviated labels.
  // (Even if upstream mapping changes, we keep customer-facing readability.)
  const mapDe: Record<string, string> = {
    U_test: 'Prüfspannung',
    'U0/U': 'Nennspannung',
    'U_0/U': 'Nennspannung',
    T: 'Temperaturbereich',
    T_op: 'Leitertemperatur (max.)',
    T_sc: 'Kurzschlusstemperatur (max.)',
    T_lay: 'Minimale Verlegetemperatur',
    T_st: 'Minimale Lagertemperatur',
    CPR: 'CPR-Klasse',
    FR: 'Flammhemmend',
  };

  const mapEn: Record<string, string> = {
    U_test: 'Test voltage',
    'U0/U': 'Rated voltage',
    'U_0/U': 'Rated voltage',
    T: 'Operating temperature range',
    T_op: 'Conductor temperature (max.)',
    T_sc: 'Short-circuit temperature (max.)',
    T_lay: 'Minimum laying temperature',
    T_st: 'Minimum storage temperature',
    CPR: 'CPR class',
    FR: 'Flame retardant',
  };

  const mapped = (locale === 'de' ? mapDe[l] : mapEn[l]) || '';
  return mapped || label;
}

function technicalFullLabel(args: { key: string; excelKey: string; locale: 'en' | 'de' }): string {
  const k = normalizeValue(args.key);

  if (args.locale === 'de') {
    // Prefer stable internal keys (from columnMapping.key) to translate Technical Data labels.
    switch (k) {
      case 'DI':
        return 'Durchmesser über Isolierung';
      case 'RI':
        return 'DC-Leiterwiderstand (20 °C)';
      case 'Wi':
        return 'Isolationsdicke';
      case 'Ibl':
        return 'Strombelastbarkeit in Luft (trefoil)';
      case 'Ibe':
        return 'Strombelastbarkeit im Erdreich (trefoil)';
      case 'Ik_cond':
        return 'Kurzschlussstrom Leiter';
      case 'Ik_screen':
        return 'Kurzschlussstrom Schirm';
      case 'Wm':
        return 'Manteldicke';
      case 'Rbv':
        return 'Biegeradius (min.)';
      case 'Ø':
        return 'Außen-Ø';
      case 'Fzv':
        return 'Zugkraft (max.)';
      case 'G':
        return 'Gewicht';
      case 'Cond':
      case 'conductor':
        return 'Leiter';
      case 'shape':
        return 'Leiterform';
      case 'insulation':
        return 'Isolierung';
      case 'sheath':
        return 'Mantel';
      case 'cap':
        return 'Kapazität';
      case 'ind_trefoil':
        return 'Induktivität (trefoil)';
      case 'ind_air_flat':
        return 'Induktivität (Luft, flach)';
      case 'ind_ground_flat':
        return 'Induktivität (Erdreich, flach)';
      case 'X':
        return 'Reaktanz';
      case 'test_volt':
        return 'Prüfspannung';
      case 'rated_volt':
        return 'Nennspannung';
      case 'temp_range':
        return 'Temperaturbereich';
      case 'max_op_temp':
        return 'Leitertemperatur (max.)';
      case 'max_sc_temp':
        return 'Kurzschlusstemperatur (max.)';
      case 'min_store_temp':
        return 'Minimale Lagertemperatur';
      case 'min_lay_temp':
        return 'Minimale Verlegetemperatur';
      case 'cpr':
        return 'CPR-Klasse';
      case 'flame':
        return 'Flammhemmend';
      case 'packaging':
        return 'Verpackung';
      case 'ce':
        return 'CE-Konformität';
      case 'norm':
        return 'Norm';
      case 'standard':
        return 'Standard';
      case 'D_screen':
        return 'Durchmesser über Schirm';
      case 'S_screen':
        return 'Metallischer Schirm';
      default:
        break;
    }

    // Fallback: best-effort translation from the raw Excel header (prevents English in DE PDFs).
    const raw = normalizeValue(args.excelKey);
    if (!raw) return '';
    return raw
      .replace(/\(approx\.?\)/gi, '(ca.)')
      .replace(/\bcapacitance\b/gi, 'Kapazität')
      .replace(/\binductance\b/gi, 'Induktivität')
      .replace(/\breactance\b/gi, 'Reaktanz')
      .replace(/\btest voltage\b/gi, 'Prüfspannung')
      .replace(/\brated voltage\b/gi, 'Nennspannung')
      .replace(/\boperating temperature range\b/gi, 'Temperaturbereich')
      .replace(/\bminimum sheath thickness\b/gi, 'Manteldicke (min.)')
      .replace(/\bsheath thickness\b/gi, 'Manteldicke')
      .replace(/\bnominal insulation thickness\b/gi, 'Isolationsdicke (nom.)')
      .replace(/\binsulation thickness\b/gi, 'Isolationsdicke')
      .replace(/\bdc resistance at 20\s*°?c\b/gi, 'DC-Leiterwiderstand (20 °C)')
      .replace(/\bouter diameter(?: of cable)?\b/gi, 'Außen-Ø')
      .replace(/\bbending radius\b/gi, 'Biegeradius')
      .replace(/\bpackaging\b/gi, 'Verpackung')
      .replace(/\bce\s*-?conformity\b/gi, 'CE-Konformität');
  }

  // EN: keep as-is (Excel headers are already English).
  return normalizeValue(args.excelKey);
}

function compactNumericForLocale(value: string, locale: 'en' | 'de'): string {
  const v = normalizeValue(value);
  if (!v) return '';
  
  // Handle special text patterns like "15xD (Single core); 12xD (Multi core)"
  // Compact to "15/12xD" or "10xD" for single values
  if (/\d+xD/.test(v)) {
    // Extract all number+xD patterns
    const numbers = [];
    const matches = Array.from(v.matchAll(/(\d+)xD/g));
    for (let i = 0; i < matches.length; i++) numbers.push(matches[i][1]);
    if (numbers.length > 0) {
      // Remove duplicates while preserving order
      const unique: string[] = [];
      for (const num of numbers) {
        if (!unique.includes(num)) {
          unique.push(num);
        }
      }
      return unique.join('/') + 'xD';
    }
  }
  
  // Normalize decimals for the target locale if it looks numeric-ish.
  const hasDigit = /\d/.test(v);
  if (!hasDigit) return v;
  const trimmed = v.replace(/\s+/g, ' ').trim();
  // Keep ranges like "1.2–3.4" or "1.2-3.4".
  const parts = trimmed.split(/(–|-)/);
  const out = parts.map(p => {
    if (p === '–' || p === '-') return p;
    const s = p.trim();
    if (!/^-?\d+(?:[\.,]\d+)?$/.test(s)) return p;
    const n = s.replace(/,/g, '.');

    // Datasheets: do NOT use k/M suffixes (can be misleading for engineering values).
    // Only trim trailing zeros for compactness.
    const compact = n
      .replace(/\.0+$/, '')
      .replace(/(\.\d*?)0+$/, '$1')
      .replace(/\.$/, '');

    // Preserve leading "+" when present (typical for temperature cells like "+90").
    const hadPlus = /^\+/.test(s);
    const withPlus = hadPlus && !/^\+/.test(compact) ? `+${compact}` : compact;
    return locale === 'de' ? withPlus.replace(/\./g, ',') : withPlus;
  });
  return out.join('');
}

function compactCellForDenseTable(value: string, unit: string | undefined, locale: 'en' | 'de'): string {
  let v = normalizeValue(value);
  if (!v) return '';
  const u = normalizeValue(unit || '');
  if (u) {
    // Remove unit occurrences from the cell value (unit is already in the header).
    const esc = u.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    v = v.replace(new RegExp(`\\s*${esc}\\b`, 'ig'), '').trim();
    // Common composite units appear in the data cells too; strip them.
    v = v
      .replace(/\bkg\s*\/\s*km\b/gi, '')
      .replace(/\bohm\s*\/\s*km\b/gi, '')
      .replace(/\bΩ\s*\/\s*km\b/gi, '')
      .replace(/\bu\s*f\s*\/\s*km\b/gi, '')
      .replace(/\bmh\s*\/\s*km\b/gi, '')
      .replace(/\bkA\b/gi, '')
      .replace(/\bmm\b/gi, '')
      .replace(/\bkv\b/gi, '')
      .replace(/\b°?c\b/gi, '')
      .replace(/\s+/g, ' ')
      .trim();
  }

  // Normalize common separators to compact but readable tokens.
  // Example: "-35  - +90" -> "-35-+90", "-40°C / +90°C" -> "-40/+90".
  v = v
    .replace(/\s*–\s*/g, '-')
    .replace(/\s*-\s*/g, '-')
    .replace(/\s*\/\s*/g, '/')
    .replace(/\s+/g, ' ')
    .trim();

  return compactNumericForLocale(v, locale);
}

function normalizeVoltageLabel(raw: string): string {
  const v = normalizeValue(raw);
  if (!v) return '';
  // Keep common MV/HV notation like "6/10" or "12/20".
  const cleaned = v.replace(/\s+/g, ' ');
  if (/\bkv\b/i.test(cleaned)) return cleaned.replace(/\bkv\b/i, 'kV');
  // If purely numeric-ish (e.g. "20"), add unit.
  const num = cleaned.match(/\d+(?:[\.,]\d+)?(?:\s*\/\s*\d+(?:[\.,]\d+)?)?/);
  if (!num) return cleaned;
  // If the string already contains other words, keep as-is.
  if (/[a-z]/i.test(cleaned)) return cleaned;
  return `${cleaned} kV`;
}

function parseVoltageSortKey(voltageLabel: string): number {
  const v = normalizeVoltageLabel(voltageLabel);
  // Sort by the last number (e.g. 6/10 -> 10, 12/20 -> 20)
  const nums = v
    .replace(/,/g, '.')
    .match(/\d+(?:\.\d+)?/g)
    ?.map(n => Number(n))
    .filter(n => Number.isFinite(n));
  if (!nums || nums.length === 0) return Number.POSITIVE_INFINITY;
  return nums[nums.length - 1];
}

function formatExcelHeaderLabel(key: string, unit?: string): string {
  const k = normalizeValue(key);
  if (!k) return '';
  const u = normalizeValue(unit || '');

  // Prefer compact but clear labels.
  const compact = k
    .replace(/\s*\(approx\.?\)\s*/gi, ' (approx.) ')
    .replace(/\s+/g, ' ')
    .trim();

  if (!u) return compact;
  // Avoid double units like "(mm) mm".
  if (new RegExp(`\\(${u.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')}\\)`, 'i').test(compact)) return compact;
  return `${compact} (${u})`;
}

function formatExcelCellValue(value: string, unit?: string, opts?: { appendUnit?: boolean }): string {
  const v = normalizeValue(value);
  if (!v) return '';
  const u = normalizeValue(unit || '');
  if (!u) return v;
  const appendUnit = opts?.appendUnit ?? true;
  // Only auto-append unit for pure numbers; many Excel cells already contain units.
  if (!appendUnit) return v;
  return looksNumeric(v) ? `${v} ${u}` : v;
}

function buildExcelModel(args: {
  product: ProductData;
  locale: 'en' | 'de';
}): { ok: boolean; technicalItems: KeyValueItem[]; voltageTables: VoltageTableModel[] } {
  const match = findExcelForProduct(args.product);
  if (!match || match.rows.length === 0) return { ok: false, technicalItems: [], voltageTables: [] };

  const units = match.units || {};
  
  // Filter rows to only include compatible column structures
  // This handles products that exist in multiple Excel files with different column structures
  const rows = match.rows;
  
  // Find the row with most columns as sample
  let sample = rows.find(r => r && Object.keys(r).length > 0) || {};
  let maxColumns = Object.keys(sample).filter(k => k && k !== 'Part Number' && k !== 'Units').length;
  
  for (const r of rows) {
    const cols = Object.keys(r).filter(k => k && k !== 'Part Number' && k !== 'Units').length;
    if (cols > maxColumns) {
      sample = r;
      maxColumns = cols;
    }
  }
  
  // Map Excel column names to our compact/standardized keys.
  // This mapping covers all common Excel column names found in the source files
  // IMPORTANT: More specific patterns must come before generic ones to avoid false matches
  const columnMapping: Record<string, { header: string; unit: string; key: string }> = {
    // Cross-section and voltage (these are used for grouping, not shown in table) - MUST COME FIRST
    'number of cores and cross-section': { header: 'Cross-section', unit: '', key: 'cross_section' },
    'ross section conductor': { header: 'Cross-section', unit: '', key: 'cross_section' },
    
    // The 13 required headers (exact order as specified)
    'diameter over insulation': { header: 'DI', unit: 'mm', key: 'DI' },
    'diameter over insulation (approx.)': { header: 'DI', unit: 'mm', key: 'DI' },
    'dc resistance at 20 °C': { header: 'RI', unit: 'Ohm/km', key: 'RI' },
    'dc resistance at 20°C': { header: 'RI', unit: 'Ohm/km', key: 'RI' },
    'resistance conductor': { header: 'RI', unit: 'Ohm/km', key: 'RI' },
    'maximum resistance of conductor': { header: 'RI', unit: 'Ohm/km', key: 'RI' },
    'insulation thickness': { header: 'Wi', unit: 'mm', key: 'Wi' },
    'nominal insulation thickness': { header: 'Wi', unit: 'mm', key: 'Wi' },
    'current ratings in air, trefoil': { header: 'Ibl', unit: 'A', key: 'Ibl' },
    'current ratings in air, trefoil*': { header: 'Ibl', unit: 'A', key: 'Ibl' },
    'current ratings in ground, trefoil': { header: 'Ibe', unit: 'A', key: 'Ibe' },
    'current ratings in ground, trefoil*': { header: 'Ibe', unit: 'A', key: 'Ibe' },
    'conductor shortcircuit current': { header: 'Ik', unit: 'kA', key: 'Ik_cond' },
    'screen shortcircuit current': { header: 'Ik', unit: 'kA', key: 'Ik_screen' },
    'sheath thickness': { header: 'Wm', unit: 'mm', key: 'Wm' },
    'minimum sheath thickness': { header: 'Wm', unit: 'mm', key: 'Wm' },
    'nominal sheath thickness': { header: 'Wm', unit: 'mm', key: 'Wm' },
    'bending radius': { header: 'Rbv', unit: 'mm', key: 'Rbv' },
    'bending radius (min.)': { header: 'Rbv', unit: 'mm', key: 'Rbv' },
    'outer diameter': { header: 'Ø', unit: 'mm', key: 'Ø' },
    'outer diameter (approx.)': { header: 'Ø', unit: 'mm', key: 'Ø' },
    'outer diameter of cable': { header: 'Ø', unit: 'mm', key: 'Ø' },
    // Pulling force is typically expressed in N in datasheets.
    // Some sources appear to store values in kN (e.g. 4.5), we normalize in the cell getter.
    'pulling force': { header: 'Fzv', unit: 'N', key: 'Fzv' },
    'max. pulling force': { header: 'Fzv', unit: 'N', key: 'Fzv' },
    // Conductor material (we render this as a single column)
    'conductor aluminum': { header: 'Cond.', unit: '', key: 'Cond' },
    'conductor copper': { header: 'Cond.', unit: '', key: 'Cond' },
    'weight': { header: 'G', unit: 'kg/km', key: 'G' },
    'weight (approx.)': { header: 'G', unit: 'kg/km', key: 'G' },
    'cable weight': { header: 'G', unit: 'kg/km', key: 'G' },
    
    // Additional technical columns (to include ALL Excel data)
    // Specific material/property columns must come before generic 'conductor' pattern
    'conductor diameter (approx.)': { header: 'Conductor diameter', unit: 'mm', key: 'cond_diam' },
    'conductor diameter': { header: 'Conductor diameter', unit: 'mm', key: 'cond_diam' },
    'diameter conductor': { header: 'Conductor diameter', unit: 'mm', key: 'cond_diam' },
    'diameter over screen': { header: 'Diameter over screen', unit: 'mm', key: 'D_screen' },
    'metallic screen mm2': { header: 'Metallic screen', unit: 'mm2', key: 'S_screen' },
    'metallic screen': { header: 'Metallic screen', unit: 'mm2', key: 'S_screen' },
    'reactance': { header: 'Reactance', unit: 'Ohm/km', key: 'X' },
    'capacitance (approx.)': { header: 'Capacitance', unit: 'uF/km', key: 'cap' },
    'capacitance': { header: 'Capacitance', unit: 'uF/km', key: 'cap' },
    'inductance, trefoil (approx.)': { header: 'Inductance trefoil', unit: 'mH/km', key: 'ind_trefoil' },
    'inductance, trefoil': { header: 'Inductance trefoil', unit: 'mH/km', key: 'ind_trefoil' },
    'inductance in air, flat (approx.)': { header: 'Inductance air flat', unit: 'mH/km', key: 'ind_air_flat' },
    'inductance in air, flat': { header: 'Inductance air flat', unit: 'mH/km', key: 'ind_air_flat' },
    'inductance in ground, flat (approx.)': { header: 'Inductance ground flat', unit: 'mH/km', key: 'ind_ground_flat' },
    'inductance in ground, flat': { header: 'Inductance ground flat', unit: 'mH/km', key: 'ind_ground_flat' },
    'current ratings in air, flat': { header: 'Current air flat', unit: 'A', key: 'cur_air_flat' },
    'current ratings in air, flat*': { header: 'Current air flat', unit: 'A', key: 'cur_air_flat' },
    'current ratings in ground, flat': { header: 'Current ground flat', unit: 'A', key: 'cur_ground_flat' },
    'current ratings in ground, flat*': { header: 'Current ground flat', unit: 'A', key: 'cur_ground_flat' },
    'heating time constant, trefoil*': { header: 'Heating time trefoil', unit: 's', key: 'heat_trefoil' },
    'heating time constant, trefoil': { header: 'Heating time trefoil', unit: 's', key: 'heat_trefoil' },
    'heating time constant, flat*': { header: 'Heating time flat', unit: 's', key: 'heat_flat' },
    'heating time constant, flat': { header: 'Heating time flat', unit: 's', key: 'heat_flat' },
    
    // Temperature and other technical data
    'maximal operating conductor temperature': { header: 'Max operating temp', unit: '°C', key: 'max_op_temp' },
    'maximal short-circuit temperature': { header: 'Max short-circuit temp', unit: '°C', key: 'max_sc_temp' },
    'operating temperature range': { header: 'Operating temp range', unit: '°C', key: 'temp_range' },
    'minimal storage temperature': { header: 'Min storage temp', unit: '°C', key: 'min_store_temp' },
    'minimal temperature for laying': { header: 'Min laying temp', unit: '°C', key: 'min_lay_temp' },
    'test voltage': { header: 'Test voltage', unit: 'kV', key: 'test_volt' },
    'rated voltage': { header: 'Rated voltage', unit: 'kV', key: 'rated_volt' },
    
    // Material and specification data
    // Note: More specific patterns must come before generic ones to avoid conflicts
    
    // Conductor description/type (keep as technical/meta data, not as material column)
    'conductor': { header: 'Conductor', unit: '', key: 'conductor' },
    
    // Screen and tape columns
    'copper wire screen and tape': { header: 'Copper screen', unit: '', key: 'copper_screen' },
    'CUScreen': { header: 'Copper screen', unit: '', key: 'copper_screen' },
    'conductive tape below screen': { header: 'Conductive tape below', unit: '', key: 'tape_below' },
    'non conducting tape above screen': { header: 'Non-conductive tape above', unit: '', key: 'tape_above' },
    'al foil': { header: 'Al foil', unit: '', key: 'al_foil' },
    
    // Material properties
    'shape of conductor': { header: 'Conductor shape', unit: '', key: 'shape' },
    'colour of insulation': { header: 'Insulation color', unit: '', key: 'color_ins' },
    'colour of sheath': { header: 'Sheath color', unit: '', key: 'color_sheath' },
    'insulation': { header: 'Insulation', unit: '', key: 'insulation' },
    'sheath': { header: 'Sheath', unit: '', key: 'sheath' },
    'norm': { header: 'Norm', unit: '', key: 'norm' },
    'standard': { header: 'Standard', unit: '', key: 'standard' },
    'cpr class': { header: 'CPR class', unit: '', key: 'cpr' },
    'flame retardant': { header: 'Flame retardant', unit: '', key: 'flame' },
    'self-extinguishing of single cable': { header: 'Flame retardant', unit: '', key: 'flame' },
    'packaging': { header: 'Packaging', unit: '', key: 'packaging' },
    'ce-conformity': { header: 'CE conformity', unit: '', key: 'ce' },
    'rohs/reach': { header: 'RoHS/REACH', unit: '', key: 'rohs_reach' },
  };

  // Get all Excel keys from sample
  const excelKeys = Object.keys(sample).filter(k => k && k !== 'Part Number' && k !== 'Units');

  // Find which Excel keys match our mapping
  const matchedColumns: Array<{ excelKey: string; mapping: { header: string; unit: string; key: string } }> = [];
  for (const excelKey of excelKeys) {
    const normalized = normalizeValue(excelKey).toLowerCase();
    for (const [pattern, mapping] of Object.entries(columnMapping)) {
      if (normalized === pattern.toLowerCase() || new RegExp(pattern, 'i').test(normalized)) {
        matchedColumns.push({ excelKey, mapping });
        break;
      }
    }
  }
  
  // Deduplicate by mapping.key to avoid duplicate columns
  const seenKeys = new Set<string>();
  const deduplicated: typeof matchedColumns = [];
  for (const item of matchedColumns) {
    if (!seenKeys.has(item.mapping.key)) {
      seenKeys.add(item.mapping.key);
      deduplicated.push(item);
    }
  }
  matchedColumns.length = 0;
  matchedColumns.push(...deduplicated);

  // Separate into 13 required headers vs additional columns
  const requiredHeaderKeys = ['DI', 'RI', 'Wi', 'Ibl', 'Ibe', 'Ik_cond', 'Wm', 'Rbv', 'Ø', 'Fzv', 'Cond', 'G'];
  const isRequiredHeader = (key: string) => requiredHeaderKeys.includes(key);

  // Filter rows to only include those with the same column structure as sample
  const sampleKeys = Object.keys(sample).filter(k => k && k !== 'Part Number' && k !== 'Units').sort();
  const compatibleRows = rows.filter(r => {
    const rKeys = Object.keys(r).filter(k => k && k !== 'Part Number' && k !== 'Units').sort();
    return JSON.stringify(rKeys) === JSON.stringify(sampleKeys);
  });

  if (compatibleRows.length === 0) return { ok: false, technicalItems: [], voltageTables: [] };

  const csKey =
    guessColumnKey(sample, [/number of cores and cross-section/i, /cross.?section/i, /ross section conductor/i]) || null;
  const voltageKey =
    guessColumnKey(sample, [/rated voltage/i, /voltage rating/i, /nennspannung/i, /spannungs/i]) || null;

  if (!csKey) return { ok: false, technicalItems: [], voltageTables: [] };

  // Group rows by voltage rating
  const byVoltage = new Map<string, number[]>();
  for (let i = 0; i < compatibleRows.length; i++) {
    const cs = normalizeValue(String(compatibleRows[i]?.[csKey] ?? ''));
    if (!cs) continue;
    const rawV = voltageKey ? normalizeValue(String(compatibleRows[i]?.[voltageKey] ?? '')) : '';
    const voltageLabel = normalizeVoltageLabel(rawV || '');
    const key = voltageLabel || (args.locale === 'de' ? 'Spannung unbekannt' : 'Voltage unknown');
    const arr = byVoltage.get(key) ?? [];
    arr.push(i);
    byVoltage.set(key, arr);
  }

  const voltageTables: VoltageTableModel[] = [];
  const technicalItems: KeyValueItem[] = [];
  
  const voltageKeysSorted = Array.from(byVoltage.keys()).sort((a, b) => {
    const na = parseVoltageSortKey(a);
    const nb = parseVoltageSortKey(b);
    if (na !== nb) return na - nb;
    return a.localeCompare(b);
  });

  // Track which columns are constant across ALL voltage groups (global constants)
  const globalConstantColumns = new Set<string>();
  
  // First pass: identify columns that are constant across all rows
  for (const { excelKey, mapping } of matchedColumns) {
    const values = compatibleRows.map(r => normalizeValue(String(r?.[excelKey] ?? ''))).filter(Boolean);
    const unique = Array.from(new Set(values.map(v => v.toLowerCase())));
    
    if (unique.length === 1 && values.length > 0) {
      globalConstantColumns.add(excelKey);

      // Global constants belong to TECHNICAL DATA (shown once).
      const unit = normalizeUnit(units[excelKey] || mapping.unit || '');
      const labelBase = technicalFullLabel({ key: mapping.key, excelKey, locale: args.locale });
      const label = formatExcelHeaderLabel(labelBase, unit);
      const value = compactCellForDenseTable(values[0], unit, args.locale);
      const existing = technicalItems.find(t => t.label === label);
      if (!existing) technicalItems.push({ label, value, unit });
    }
  }

  // Second pass: for each voltage group, separate constant vs variable columns
  for (const vKey of voltageKeysSorted) {
    const indices = byVoltage.get(vKey) || [];
    if (!indices.length) continue;

    const crossSections = indices.map(idx => normalizeValue(String(compatibleRows[idx]?.[csKey] ?? '')));

    // Meta items: keep a consistent, compact set across products.
    // This is the "voltage-group meta header" (parameter block) above each table.
    const metaItems: KeyValueItem[] = [];
    const metaCandidates = new Map<string, KeyValueItem>();
    if (voltageKey) {
      const rawV = normalizeValue(String(compatibleRows[indices[0]]?.[voltageKey] ?? ''));
      metaItems.push({
        label: args.locale === 'de' ? 'Spannung' : 'Voltage',
        value: normalizeVoltageLabel(rawV || ''),
      });
    }

    // Which non-table fields we want to show consistently per voltage group.
    const metaKeyPriority = [
      'test_volt',
      'temp_range',
      'max_op_temp',
      'max_sc_temp',
      'min_lay_temp',
      'min_store_temp',
      'cpr',
      'flame',
    ];
    const metaKeyPrioritySet = new Set(metaKeyPriority);

    // Cross-section table is limited to the core industry columns (compact for A4).
    // To avoid “too little data” we always render the full set of core columns in the table,
    // even when a value is constant across the voltage group.
    const tableColumns: Array<{ excelKey: string; mapping: { header: string; unit: string; key: string } }> = [];

    const denseTableKeyOrder = [
      // Conductor should be the first technical column (after designation).
      // This improves scanability in the cable industry (material/type is a primary discriminator).
      'Cond',
      // Next highest priority for LV/MV: conductor shape/type (RE/RM/RMV/...).
      'shape',

      // Electrical properties (when available) – high value for MV/HV engineering.
      'cap',
      'X',

      'DI',
      'RI',
      'Wi',
      'Ibl',
      'Ibe',
      'Ik_cond',
      'Wm',
      'Rbv',
      'Ø',

      // Extra high-value dimensions/metal screen info (when available): common in MV/HV.
      // Keep close to Ø to support engineers comparing layer builds.
      'D_screen',
      'S_screen',

      'Fzv',
      'G',
    ] as const;
    const denseTableKeys = new Set<string>(denseTableKeyOrder);

    // Extra data handling:
    // - global constants => TECHNICAL DATA
    // - voltage-group constants => metaItems
    // - voltage-group varying (non-core) => metaItems as ranges/lists (keeps A4 compact)

    // Pre-scan: detect if bending radius is expressed as xD (common in LV/Solar sheets)
    // so we can label the unit correctly (Rbv [xD] instead of Rbv [mm]).
    // Detect bending radius representation (mm vs xD) from the matched Excel column.
    const bendingRadiusKey = matchedColumns.find(c => c.mapping.key === 'Rbv')?.excelKey || null;
    let bendUnitOverride = '';
    if (bendingRadiusKey) {
      const bendVals = indices
        .map(idx => normalizeValue(String(compatibleRows[idx]?.[bendingRadiusKey] ?? '')))
        .filter(Boolean);
      if (bendVals.some(v => /\bxD\b/i.test(v))) bendUnitOverride = 'xD';
    }

    // 1) Collect mapped columns.
    for (const { excelKey, mapping } of matchedColumns) {
      // Skip cross-section and voltage keys (these are used for grouping)
      if (excelKey === csKey || excelKey === voltageKey) continue;

      // Get values for this voltage group
      const values = indices.map(idx => normalizeValue(String(compatibleRows[idx]?.[excelKey] ?? ''))).filter(Boolean);
      
      if (values.length > 0) {
        const unique = Array.from(new Set(values.map(v => v.toLowerCase())));
        let unit = normalizeUnit(units[excelKey] || mapping.unit || '');
        if (mapping.key === 'Rbv' && bendUnitOverride) unit = bendUnitOverride;

        // Always keep the core 13 columns in the table (even if constant).
        if (denseTableKeys.has(mapping.key)) {
          tableColumns.push({ excelKey, mapping });
          continue;
        }

        // For meta header: collect candidates, but only *display* a consistent subset.
        // Global constants are normally in TECHNICAL DATA, but we still allow them
        // into the voltage meta block if they are part of the priority set.
        if (globalConstantColumns.has(excelKey) && !metaKeyPrioritySet.has(mapping.key)) {
          continue;
        }

        const value =
          unique.length === 1
            ? compactCellForDenseTable(values[0], unit, args.locale)
            : summarizeSmartOptions(excelKey, values);

        // Meta header: keep labels fully readable (no abbreviations).
        // Units are shown separately by the meta grid.
        const label = metaFullLabel({ key: mapping.key, excelKey, locale: args.locale });

        metaCandidates.set(mapping.key, { label, value, unit });
      }
    }

    // 1b) Materialize meta items in a stable order.
    // This keeps LV/MV/HV tables visually consistent (no "MV has much more in front").
    for (const k of metaKeyPriority) {
      const item = metaCandidates.get(k);
      if (item && item.label && item.value) metaItems.push(item);
    }

    // 2) Build the compact cross-section table.
    // If a column is not available in the Excel source and we cannot derive it safely,
    // we omit it (empty columns waste A4 width and reduce readability).
    const mappedByKey = new Map<string, { excelKey: string; mapping: { header: string; unit: string; key: string } }>();
    for (const c of tableColumns) {
      if (!mappedByKey.has(c.mapping.key)) mappedByKey.set(c.mapping.key, c);
    }

    // Helper keys for derived values.
    // We derive DI (diameter over insulation) from Ø and Wm when DI is missing.
    const outerDiameterKey = (mappedByKey.get('Ø')?.excelKey || '') || null;
    const sheathThicknessKey = (mappedByKey.get('Wm')?.excelKey || '') || null;

    const canDeriveDenseKey = (k: (typeof denseTableKeyOrder)[number]): boolean => {
      if (k === 'DI') return Boolean(outerDiameterKey && sheathThicknessKey);
      if (k === 'Cond') return true; // derived from product designation when missing
      return false;
    };

    const orderedTableColumns = denseTableKeyOrder
      .filter(k => mappedByKey.has(k) || canDeriveDenseKey(k))
      .map(k => {
        const existing = mappedByKey.get(k);
        if (existing) return existing;
        return {
          excelKey: '',
          mapping: { header: k, unit: '', key: k },
        };
      });

    // Debug: Check for duplicate keys
    if (process.env.PDF_DEBUG_EXCEL === '1') {
      const keys = tableColumns.map(c => c.mapping.key);
      const duplicates = keys.filter((k, i) => keys.indexOf(k) !== i);
      if (duplicates.length > 0) {
        console.log(`[debug] Duplicate keys found: ${duplicates.join(', ')}`);
        console.log(`[debug] All columns: ${tableColumns.map(c => c.mapping.key + '(' + c.mapping.header + ')').join(', ')}`);
      }
    }

    const columns = orderedTableColumns.map(({ excelKey, mapping }) => {
      // Default units for the compact column set (used when an Excel unit is missing).
      // We keep Excel units when available.
      const defaultUnitByKey: Record<string, string> = {
        DI: 'mm',
        RI: 'Ohm/km',
        Wi: 'mm',
        Ibl: 'A',
        Ibe: 'A',
        Ik_cond: 'kA',
        Wm: 'mm',
        Rbv: 'mm',
        'Ø': 'mm',
        Fzv: 'N',
        G: 'kg/km',
      };

       let unit = normalizeUnit((excelKey ? units[excelKey] : '') || mapping.unit || defaultUnitByKey[mapping.key] || '');
       if (mapping.key === 'Rbv' && bendUnitOverride) unit = bendUnitOverride;

      return {
        key: mapping.key,
        // Keep labels compact for dense tables; headerLabelFor() will use these.
        label: denseAbbrevLabel({ key: mapping.key, locale: args.locale, unit, withUnit: true }) || formatExcelHeaderLabel(excelKey, unit),
        get: (rowIndex: number) => {
          const srcRowIndex = indices[rowIndex];
          const raw = excelKey ? normalizeValue(String(compatibleRows[srcRowIndex]?.[excelKey] ?? '')) : '';
          const unitLocal = unit;

           // LV sheets (e.g. NA2XY): current ratings (Ibl/Ibe) and short-circuit current (Ik)
           // are typically not part of the source Excel. Keep cells empty (don’t guess).
           // However, for DI we can derive a usable engineering approximation.

          // Derived values (only when the source column is missing/empty).
          // DI (diameter over insulation): approx. from Ø and Wm when available.
          if (mapping.key === 'DI' && !raw && outerDiameterKey && sheathThicknessKey) {
            const odRaw = normalizeValue(String(compatibleRows[srcRowIndex]?.[outerDiameterKey] ?? ''));
            const wmRaw = normalizeValue(String(compatibleRows[srcRowIndex]?.[sheathThicknessKey] ?? ''));
            const od = parseNumericOption(odRaw);
            const wm = parseNumericOption(wmRaw);
            if (od !== null && wm !== null) {
              const di = od - 2 * wm;
              if (Number.isFinite(di) && di > 0) return `~${compactNumericForLocale(String(di), args.locale)}`;
            }
          }

          // Conductor material: if not present in Excel, derive from part number prefix.
          // NA… => Al, N… => Cu (common cable designation pattern in this dataset).
          if (mapping.key === 'Cond' && !raw) {
            const pn = normalizeExcelKey(args.product.name || args.product.slug || args.product.sku || '');
            if (/^NA/.test(pn)) return 'Al';
            if (/^N/.test(pn)) return 'Cu';
          }

          // If bending radius is given as xD, keep it as-is (unit label reflects xD).
          if (mapping.key === 'Rbv' && /\bxD\b/i.test(raw)) return compactNumericForLocale(raw, args.locale);

        // HV source: "Min. bending radius" appears to be stored in meters (e.g. 1.70).
        // Convert to mm when we label it as mm.
        if (mapping.key === 'Rbv' && unitLocal.toLowerCase() === 'mm') {
          const n = parseNumericOption(raw);
          const looksLikeMeters = n !== null && n > 0 && n < 50 && /[\.,]\d{1,3}/.test(raw) && !/\dxD/i.test(raw);
          if (looksLikeMeters) return compactNumericForLocale(String(Math.round(n * 1000)), args.locale);
        }

        // Pulling force: some sources appear to store kN values (e.g. 4.5) without unit.
        // When header/unit is N and the value is small, normalize to N.
        if (mapping.key === 'Fzv' && unitLocal.toLowerCase() === 'n') {
          const n = parseNumericOption(raw);
          const looksLikeKN = n !== null && n > 0 && n < 100 && !/\bN\b/i.test(raw) && !/\bkN\b/i.test(raw);
          if (looksLikeKN) return compactNumericForLocale(String(Math.round(n * 1000)), args.locale);
        }

          return compactCellForDenseTable(raw, unitLocal, args.locale);
        },
      };
    });

    voltageTables.push({ voltageLabel: vKey, metaItems, crossSections, columns });
    
    // Debug: Show columns for this voltage
    if (process.env.PDF_DEBUG_EXCEL === '1') {
      console.log(`[debug] Voltage ${vKey}: ${columns.length} columns`);
      console.log(`[debug] Columns: ${columns.map(c => c.key).join(', ')}`);
    }
  }

  technicalItems.sort((a, b) => a.label.localeCompare(b.label));

  // Debug: Show technical items
  if (process.env.PDF_DEBUG_EXCEL === '1') {
    console.log(`[debug] Technical items: ${technicalItems.map(t => t.label).join(', ')}`);
  }

  return { ok: true, technicalItems, voltageTables };
}

// Unified key-value grid renderer for both metagrid and technical data
function drawKeyValueTable(args: {
  title: string;
  items: Array<{ label: string; value: string; unit?: string }>;
  locale: 'en' | 'de';
  newPage: () => number;
  getPage: () => PDFPage;
  page: PDFPage;
  y: number;
  margin: number;
  contentWidth: number;
  contentMinY: number;
  font: PDFFont;
  fontBold: PDFFont;
  navy: ReturnType<typeof rgb>;
  darkGray: ReturnType<typeof rgb>;
  mediumGray: ReturnType<typeof rgb>;
  expandLabels?: boolean; // true for metagrid, false for technical data
}): number {
  let { page, margin, contentWidth, contentMinY, font, fontBold, navy, darkGray, mediumGray } = args;
  let y = args.y;
  const items = (args.items || []).filter(i => i.label && i.value);
  if (!items.length) return y;

  const lightGray = rgb(0.9020, 0.9137, 0.9294);
  const almostWhite = rgb(0.9725, 0.9765, 0.9804);
  const expandLabels = args.expandLabels ?? false;

  // Auto-determine columns based on item count
  const cols = items.length >= 7 ? 3 : 2;
  const colWidth = contentWidth / cols;
  const cellH = 34;
  const padX = 10;
  const titleH = 18;
  const headerPadY = 10;

  const labelSize = 7.25;
  const valueSize = 8.75;
  const labelYOff = 12;
  const valueYOff = 28;

  const rows = Math.ceil(items.length / cols);
  const boxH = headerPadY + titleH + rows * cellH + headerPadY;
  const needed = boxH + 10;

  if (y - needed < contentMinY) y = args.newPage();
  page = args.getPage();

  const boxTopY = y;
  const boxBottomY = boxTopY - boxH;

  // Outer frame
  page.drawRectangle({
    x: margin,
    y: boxBottomY,
    width: contentWidth,
    height: boxH,
    borderColor: lightGray,
    borderWidth: 1,
    color: rgb(1, 1, 1),
  });

  // Title band
  page.drawRectangle({
    x: margin,
    y: boxTopY - (headerPadY + titleH),
    width: contentWidth,
    height: headerPadY + titleH,
    color: almostWhite,
  });

  if (args.title) {
    page.drawText(args.title, {
      x: margin + padX,
      y: boxTopY - (headerPadY + 12),
      size: 10,
      font: fontBold,
      color: navy,
      maxWidth: contentWidth - padX * 2,
    });
  }

  // Separator below title
  const gridTopY = boxTopY - (headerPadY + titleH);
  page.drawLine({
    start: { x: margin, y: gridTopY },
    end: { x: margin + contentWidth, y: gridTopY },
    thickness: 0.75,
    color: lightGray,
  });

  // Render grid cells
  for (let r = 0; r < rows; r++) {
    const rowTopY = gridTopY - r * cellH;
    const rowBottomY = rowTopY - cellH;

    // Zebra striping
    if (r % 2 === 0) {
      page.drawRectangle({
        x: margin,
        y: rowBottomY,
        width: contentWidth,
        height: cellH,
        color: rgb(0.99, 0.992, 0.995),
      });
    }

    // Row separator (except last)
    if (r !== rows - 1) {
      page.drawLine({
        start: { x: margin, y: rowBottomY },
        end: { x: margin + contentWidth, y: rowBottomY },
        thickness: 0.5,
        color: lightGray,
      });
    }

    for (let c = 0; c < cols; c++) {
      const idx = r * cols + c;
      const item = items[idx];
      const x0 = margin + c * colWidth;

      // Column separator (except last) - draw from grid top to bottom for full height
      if (c !== cols - 1) {
        page.drawLine({
          start: { x: x0 + colWidth, y: gridTopY },
          end: { x: x0 + colWidth, y: boxBottomY + headerPadY },
          thickness: 0.5,
          color: lightGray,
        });
      }

      if (!item) continue;

      const labelText = expandLabels ? expandMetaLabel(item.label, args.locale) : item.label;
      const valueText = item.unit ? `${item.value} ${item.unit}` : item.value;

      const maxW = colWidth - padX * 2 - 2;
      const labelOneLine = ellipsizeToWidth(labelText, fontBold, labelSize, maxW);
      const valueOneLine = ellipsizeToWidth(valueText, font, valueSize, maxW);

      page.drawText(labelOneLine, {
        x: x0 + padX,
        y: rowTopY - labelYOff,
        size: labelSize,
        font: fontBold,
        color: mediumGray,
      });
      page.drawText(valueOneLine, {
        x: x0 + padX,
        y: rowTopY - valueYOff,
        size: valueSize,
        font,
        color: darkGray,
      });
    }
  }

  return Math.max(contentMinY, boxBottomY - 18);
}

// Backward compatibility wrapper for metagrid
function drawDenseMetaGrid(args: {
  title: string;
  items: Array<{ label: string; value: string; unit?: string }>;
  locale: 'en' | 'de';
  newPage: () => number;
  getPage: () => PDFPage;
  page: PDFPage;
  y: number;
  margin: number;
  contentWidth: number;
  contentMinY: number;
  font: PDFFont;
  fontBold: PDFFont;
  navy: ReturnType<typeof rgb>;
  darkGray: ReturnType<typeof rgb>;
  mediumGray: ReturnType<typeof rgb>;
}): number {
  return drawKeyValueTable({ ...args, expandLabels: true });
}

function prioritizeColumnsForDenseTable(args: {
  columns: VoltageTableModel['columns'];
}): VoltageTableModel['columns'] {
  // Priority order: compact cross-section table columns first, then all additional technical data
  const priorityOrder = [
    // Compact cross-section table columns (industry standard)
    // NOTE: The designation/configuration is the *first* table column already.
    // We put conductor material/type first among the technical columns for better scanability.
    // Note: Ik is represented by Ik_cond (conductor shortcircuit current)
    'Cond',
    'shape',
    'cap',
    'X',
    'DI',
    'RI',
    'Wi',
    'Ibl',
    'Ibe',
    'Ik_cond',
    'Wm',
    'Rbv',
    'Ø',
    // Extra high-value dimensions/metal screen info (when present)
    'D_screen',
    'S_screen',
    'Fzv',
    'G',
    
    // Additional technical columns (in logical groups)
    // Dimensions and materials
    'cond_diam', 'shape', 'conductor', 'insulation', 'sheath',
    // Temperatures
    'max_op_temp', 'max_sc_temp', 'temp_range', 'min_store_temp', 'min_lay_temp',
    // Electrical properties
    'cap', 'ind_trefoil', 'ind_air_flat', 'ind_ground_flat',
    // Current ratings (flat)
    'cur_air_flat', 'cur_ground_flat',
    // Heating time constants
    'heat_trefoil', 'heat_flat',
    // Voltage ratings
    'test_volt', 'rated_volt',
    // Colors
    'color_ins', 'color_sheath',
    // Materials and specs
    'norm', 'standard', 'cpr', 'flame', 'packaging', 'ce',
    // Screen/tape layers
    'tape_below', 'copper_screen', 'tape_above', 'al_foil',
    // Additional shortcircuit current
    'Ik_screen'
  ];
  
  return [...args.columns].sort((a, b) => {
    const ia = priorityOrder.indexOf(a.key);
    const ib = priorityOrder.indexOf(b.key);
    if (ia !== -1 && ib !== -1) return ia - ib;
    if (ia !== -1) return -1;
    if (ib !== -1) return 1;
    return a.key.localeCompare(b.key);
  });
}

function normalizeExcelKey(value: string): string {
  // Match product names/slugs and Excel "Part Number" robustly.
  // Examples:
  // - "NA2XS(FL)2Y" -> "NA2XSFL2Y"
  // - "na2xsfl2y-3" -> "NA2XSFL2Y"
  return String(value || '')
    .toUpperCase()
    .replace(/-\d+$/g, '')
    .replace(/[^A-Z0-9]+/g, '');
}

function loadExcelRows(filePath: string): ExcelRow[] {
  // We intentionally avoid adding a heavy xlsx parser dependency.
  // Instead, we use `xlsx-cli` via npx, which is already available at runtime.
  // NOTE: `xlsx-cli -j` prints the sheet name on the first line, then JSON.
  const out = execSync(`npx -y xlsx-cli -j "${filePath}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
  const trimmed = out.trim();
  const jsonStart = trimmed.indexOf('[');
  if (jsonStart < 0) return [];
  const jsonText = trimmed.slice(jsonStart);
  try {
    return JSON.parse(jsonText) as ExcelRow[];
  } catch {
    return [];
  }
}

function getExcelIndex(): Map<string, ExcelMatch> {
  if (EXCEL_INDEX) return EXCEL_INDEX;
  const idx = new Map<string, ExcelMatch>();
  for (const file of EXCEL_SOURCE_FILES) {
    if (!fs.existsSync(file)) continue;
    const rows = loadExcelRows(file);
    if (process.env.PDF_DEBUG_EXCEL === '1') {
      console.log(`[excel] loaded ${rows.length} rows from ${path.relative(process.cwd(), file)}`);
    }

    const unitsRow = rows.find(r => r && r['Part Number'] === 'Units') || null;
    const units: Record<string, string> = {};
    if (unitsRow) {
      for (const [k, v] of Object.entries(unitsRow)) {
        if (k === 'Part Number') continue;
        const unit = normalizeValue(String(v ?? ''));
        if (unit) units[k] = unit;
      }
    }

    for (const r of rows) {
      const pn = r?.['Part Number'];
      if (!pn || pn === 'Units') continue;
      const key = normalizeExcelKey(String(pn));
      if (!key) continue;
      const cur = idx.get(key);
      if (!cur) {
        idx.set(key, { rows: [r], units });
      } else {
        cur.rows.push(r);
        if (Object.keys(cur.units).length < Object.keys(units).length) cur.units = units;
      }
    }
  }
  EXCEL_INDEX = idx;
  return idx;
}

function findExcelForProduct(product: ProductData): ExcelMatch | null {
  const idx = getExcelIndex();
  const candidates = [
    product.name,
    product.slug ? product.slug.replace(/-\d+$/g, '') : '',
    product.sku,
    product.translationKey,
  ].filter(Boolean) as string[];

  if (process.env.PDF_DEBUG_EXCEL === '1') {
    const keys = candidates.map(c => normalizeExcelKey(c));
    console.log(`[excel] lookup product=${product.id} ${product.locale ?? ''} slug=${product.slug ?? ''} name=${stripHtml(product.name)} keys=${keys.join(',')}`);
  }

  for (const c of candidates) {
    const key = normalizeExcelKey(c);
    const match = idx.get(key);
    if (match && match.rows.length) return match;
  }
  return null;
}

function findExcelRowsForProduct(product: ProductData): ExcelRow[] {
  const match = findExcelForProduct(product);
  if (!match?.rows || match.rows.length === 0) return [];
  
  const rows = match.rows;
  
  // Find the row with most columns as sample
  let sample = rows.find(r => r && Object.keys(r).length > 0) || {};
  let maxColumns = Object.keys(sample).filter(k => k && k !== 'Part Number' && k !== 'Units').length;
  
  for (const r of rows) {
    const cols = Object.keys(r).filter(k => k && k !== 'Part Number' && k !== 'Units').length;
    if (cols > maxColumns) {
      sample = r;
      maxColumns = cols;
    }
  }
  
  // Filter to only rows with the same column structure as sample
  const sampleKeys = Object.keys(sample).filter(k => k && k !== 'Part Number' && k !== 'Units').sort();
  const compatibleRows = rows.filter(r => {
    const rKeys = Object.keys(r).filter(k => k && k !== 'Part Number' && k !== 'Units').sort();
    return JSON.stringify(rKeys) === JSON.stringify(sampleKeys);
  });
  
  return compatibleRows;
}

function guessColumnKey(row: ExcelRow, patterns: RegExp[]): string | null {
  const keys = Object.keys(row || {});
  
  // Try pattern-based matching first
  for (const re of patterns) {
    const k = keys.find(x => {
      const key = String(x);
      
      // Specific exclusions to prevent wrong matches
      if (re.test('conductor') && /ross section conductor/i.test(key)) return false;
      if (re.test('insulation thickness') && /Diameter over insulation/i.test(key)) return false;
      if (re.test('conductor') && !/^conductor$/i.test(key)) return false;
      if (re.test('insulation') && !/^insulation$/i.test(key)) return false;
      if (re.test('sheath') && !/^sheath$/i.test(key)) return false;
      if (re.test('norm') && !/^norm$/i.test(key)) return false;
      
      return re.test(key);
    });
    if (k) return k;
  }
  return null;
}

function hasAttr(product: ProductData, nameRe: RegExp, expectedLen?: number): boolean {
  const a = product.attributes?.find(x => nameRe.test(x.name));
  if (!a) return false;
  if (typeof expectedLen === 'number') return (a.options || []).length === expectedLen;
  return (a.options || []).length > 0;
}

function pushRowAttrIfMissing(args: {
  product: ProductData;
  name: string;
  options: string[];
  expectedLen: number;
  existsRe: RegExp;
}): void {
  const { product, name, options, expectedLen, existsRe } = args;
  if (!options.filter(Boolean).length) return;
  if (hasAttr(product, existsRe, expectedLen)) return;
  product.attributes = product.attributes || [];
  product.attributes.push({ name, options });
}

function pushAttrIfMissing(args: { product: ProductData; name: string; options: string[]; existsRe: RegExp }): void {
  const { product, name, options, existsRe } = args;
  if (!options.filter(Boolean).length) return;
  if (hasAttr(product, existsRe)) return;
  product.attributes = product.attributes || [];
  product.attributes.push({ name, options });
}

function getUniqueNonEmpty(options: string[]): string[] {
  const uniq: string[] = [];
  const seen = new Set<string>();
  for (const v of options.map(normalizeValue).filter(Boolean)) {
    const k = v.toLowerCase();
    if (seen.has(k)) continue;
    seen.add(k);
    uniq.push(v);
  }
  return uniq;
}

function ensureExcelCrossSectionAttributes(product: ProductData, locale: 'en' | 'de'): void {
  const hasCross = (product.attributes || []).some(a => /configuration|konfiguration|aufbau|bezeichnung|number of cores and cross-section|querschnitt|cross.?section|mm²|mm2/i.test(a.name) && (a.options?.length || 0) > 0);
  if (hasCross) return;

  const rows = findExcelRowsForProduct(product);
  if (!rows.length) {
    if (process.env.PDF_DEBUG_EXCEL === '1') {
      console.log(`[excel] no rows found for product ${product.id} (${product.slug ?? stripHtml(product.name)})`);
    }
    return;
  }

  // Find the cross-section column.
  const csKey =
    guessColumnKey(rows[0], [
      /number of cores and cross-section/i,
      /cross.?section/i,
      /ross section conductor/i,
    ]) || null;
  if (!csKey) {
    if (process.env.PDF_DEBUG_EXCEL === '1') {
      console.log(`[excel] rows found but no cross-section column for product ${product.id}; available keys: ${Object.keys(rows[0] || {}).slice(0, 30).join(', ')}`);
    }
    return;
  }

  // Get all technical column keys using improved detection
  const voltageKey = guessColumnKey(rows[0], [/rated voltage/i, /voltage rating/i, /spannungs/i, /nennspannung/i]);
  const outerKey = guessColumnKey(rows[0], [/outer diameter\b/i, /outer diameter.*approx/i, /outer diameter of cable/i, /außen/i]);
  const weightKey = guessColumnKey(rows[0], [/weight\b/i, /gewicht/i, /cable weight/i]);
  const dcResKey = guessColumnKey(rows[0], [/dc resistance/i, /resistance conductor/i, /leiterwiderstand/i]);

  // Additional technical columns
  const ratedVoltKey = voltageKey; // Already found above
  const testVoltKey = guessColumnKey(rows[0], [/test voltage/i, /prüfspannung/i]);
  const tempRangeKey = guessColumnKey(rows[0], [/operating temperature range/i, /temperature range/i, /temperaturbereich/i]);
  const minLayKey = guessColumnKey(rows[0], [/minimal temperature for laying/i]);
  const minStoreKey = guessColumnKey(rows[0], [/minimal storage temperature/i]);
  const maxOpKey = guessColumnKey(rows[0], [/maximal operating conductor temperature/i, /max\. operating/i]);
  const maxScKey = guessColumnKey(rows[0], [/maximal short-circuit temperature/i, /short\s*circuit\s*temperature/i]);
  const insThkKey = guessColumnKey(rows[0], [/nominal insulation thickness/i, /insulation thickness/i]);
  const sheathThkKey = guessColumnKey(rows[0], [/nominal sheath thickness/i, /minimum sheath thickness/i]);
  const maxResKey = guessColumnKey(rows[0], [/maximum resistance of conductor/i]);

  // Material and specification columns
  const conductorKey = guessColumnKey(rows[0], [/^conductor$/i]);
  const insulationKey = guessColumnKey(rows[0], [/^insulation$/i]);
  const sheathKey = guessColumnKey(rows[0], [/^sheath$/i]);
  const normKey = guessColumnKey(rows[0], [/^norm$/i, /^standard$/i]);
  const cprKey = guessColumnKey(rows[0], [/cpr class/i]);
  const rohsKey = guessColumnKey(rows[0], [/^rohs$/i]);
  const reachKey = guessColumnKey(rows[0], [/^reach$/i]);
  const packagingKey = guessColumnKey(rows[0], [/^packaging$/i]);
  const shapeKey = guessColumnKey(rows[0], [/shape of conductor/i]);
  const flameKey = guessColumnKey(rows[0], [/flame retardant/i]);
  const diamCondKey = guessColumnKey(rows[0], [/diameter conductor/i]);
  const diamInsKey = guessColumnKey(rows[0], [/diameter over insulation/i]);
  const diamScreenKey = guessColumnKey(rows[0], [/diameter over screen/i]);
  const metalScreenKey = guessColumnKey(rows[0], [/metallic screen/i]);
  const capacitanceKey = guessColumnKey(rows[0], [/capacitance/i]);
  const reactanceKey = guessColumnKey(rows[0], [/reactance/i]);
  const electricalStressKey = guessColumnKey(rows[0], [/electrical stress/i]);
  const pullingForceKey = guessColumnKey(rows[0], [/max\. pulling force/i, /pulling force/i]);
  const heatingTrefoilKey = guessColumnKey(rows[0], [/heating time constant.*trefoil/i]);
  const heatingFlatKey = guessColumnKey(rows[0], [/heating time constant.*flat/i]);
  const currentAirTrefoilKey = guessColumnKey(rows[0], [/current ratings in air.*trefoil/i]);
  const currentAirFlatKey = guessColumnKey(rows[0], [/current ratings in air.*flat/i]);
  const currentGroundTrefoilKey = guessColumnKey(rows[0], [/current ratings in ground.*trefoil/i]);
  const currentGroundFlatKey = guessColumnKey(rows[0], [/current ratings in ground.*flat/i]);
  const scCurrentCondKey = guessColumnKey(rows[0], [/conductor shortcircuit current/i]);
  const scCurrentScreenKey = guessColumnKey(rows[0], [/screen shortcircuit current/i]);
  
  const cfgName = locale === 'de' ? 'Anzahl der Adern und Querschnitt' : 'Number of cores and cross-section';
  const cfgOptions = rows
    .map(r => {
      const cs = normalizeValue(String(r?.[csKey] ?? ''));
      const v = voltageKey ? normalizeValue(String(r?.[voltageKey] ?? '')) : '';
      if (!cs) return '';
      if (!v) return cs;
      // Keep the existing config separator used by splitConfig(): "cross - voltage".
      // Add unit only if not already present.
      const vHasUnit = /\bkv\b/i.test(v);
      const vText = vHasUnit ? v : `${v} kV`;
      return `${cs} - ${vText}`;
    })
    .filter(Boolean);

  if (!cfgOptions.length) return;

  const attrs = product.attributes || [];
  attrs.push({ name: cfgName, options: cfgOptions });

  const pushRowAttr = (name: string, key: string | null, unit?: string) => {
    if (!key) return;
    const options = rows
      .map(r => normalizeValue(String(r?.[key] ?? '')))
      .map(v => (unit && v && looksNumeric(v) ? `${v} ${unit}` : v));
    if (options.filter(Boolean).length === 0) return;
    attrs.push({ name, options });
  };

  // These names are chosen so existing PDF regexes can detect them.
  pushRowAttr(locale === 'de' ? 'Außen-Ø' : 'Outer diameter', outerKey, 'mm');
  pushRowAttr(locale === 'de' ? 'Gewicht' : 'Weight', weightKey, 'kg/km');
  pushRowAttr(locale === 'de' ? 'DC-Leiterwiderstand (20C)' : 'DC resistance at 20 C', dcResKey, 'Ohm/km');

  const colValues = (key: string | null) => rows.map(r => normalizeValue(String(r?.[key ?? ''] ?? '')));

  const addConstOrSmallList = (args: { name: string; existsRe: RegExp; key: string | null }) => {
    if (!args.key) return;
    const uniq = getUniqueNonEmpty(colValues(args.key));
    if (!uniq.length) return;
    // If all rows share the same value, store as single option.
    if (uniq.length === 1) {
      pushAttrIfMissing({ product, name: args.name, options: [uniq[0]], existsRe: args.existsRe });
      return;
    }
    // Otherwise store the unique set (TECHNICAL DATA will compact it).
    pushAttrIfMissing({ product, name: args.name, options: uniq, existsRe: args.existsRe });
  };

  addConstOrSmallList({
    name: locale === 'de' ? 'Nennspannung' : 'Rated voltage',
    existsRe: /rated\s*voltage|voltage\s*rating|nennspannung|spannungsbereich/i,
    key: ratedVoltKey,
  });
  addConstOrSmallList({
    name: locale === 'de' ? 'Prüfspannung' : 'Test voltage',
    existsRe: /test\s*voltage|prüfspannung/i,
    key: testVoltKey,
  });
  addConstOrSmallList({
    name: locale === 'de' ? 'Temperaturbereich' : 'Operating temperature range',
    existsRe: /operating\s*temperature\s*range|temperature\s*range|temperaturbereich/i,
    key: tempRangeKey,
  });
  addConstOrSmallList({
    name: locale === 'de' ? 'Min. Verlegetemperatur' : 'Minimal temperature for laying',
    existsRe: /minimal\s*temperature\s*for\s*laying/i,
    key: minLayKey,
  });
  addConstOrSmallList({
    name: locale === 'de' ? 'Min. Lagertemperatur' : 'Minimal storage temperature',
    existsRe: /minimal\s*storage\s*temperature/i,
    key: minStoreKey,
  });
  addConstOrSmallList({
    name: locale === 'de' ? 'Max. Betriebstemperatur' : 'Maximal operating conductor temperature',
    existsRe: /maximal\s*operating\s*conductor\s*temperature|max\.?\s*operating/i,
    key: maxOpKey,
  });
  addConstOrSmallList({
    name: locale === 'de' ? 'Kurzschlusstemperatur (max.)' : 'Maximal short-circuit temperature',
    existsRe: /maximal\s*short-?circuit\s*temperature|short\s*circuit\s*temperature|kurzschlusstemperatur/i,
    key: maxScKey,
  });
  addConstOrSmallList({
    name: locale === 'de' ? 'Isolationsdicke (nom.)' : 'Nominal insulation thickness',
    existsRe: /nominal\s*insulation\s*thickness|insulation\s*thickness/i,
    key: insThkKey,
  });
  addConstOrSmallList({
    name: locale === 'de' ? 'Manteldicke (nom.)' : 'Nominal sheath thickness',
    existsRe: /nominal\s*sheath\s*thickness|minimum\s*sheath\s*thickness|manteldicke/i,
    key: sheathThkKey,
  });
  addConstOrSmallList({
    name: locale === 'de' ? 'Max. Leiterwiderstand' : 'Maximum resistance of conductor',
    existsRe: /maximum\s*resistance\s*of\s*conductor|max\.?\s*resistance|leiterwiderstand/i,
    key: maxResKey,
  });

  // Add additional technical data from Excel files
  addConstOrSmallList({
    name: locale === 'de' ? 'Leiter' : 'Conductor',
    existsRe: /conductor/i,
    key: conductorKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Isolierung' : 'Insulation',
    existsRe: /insulation/i,
    key: insulationKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Mantel' : 'Sheath',
    existsRe: /sheath/i,
    key: sheathKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Norm' : 'Standard',
    existsRe: /norm|standard|iec|vde/i,
    key: normKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Leiterdurchmesser' : 'Conductor diameter',
    existsRe: /diameter conductor|conductor diameter/i,
    key: diamCondKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Isolierungsdurchmesser' : 'Insulation diameter',
    existsRe: /diameter over insulation|diameter insulation/i,
    key: diamInsKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Schirmdurchmesser' : 'Screen diameter',
    existsRe: /diameter over screen|diameter screen/i,
    key: diamScreenKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Metallischer Schirm' : 'Metallic screen',
    existsRe: /metallic screen/i,
    key: metalScreenKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Max. Zugkraft' : 'Max. pulling force',
    existsRe: /max.*pulling force|pulling force/i,
    key: pullingForceKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Elektrische Spannung Leiter' : 'Electrical stress conductor',
    existsRe: /electrical stress conductor/i,
    key: electricalStressKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Elektrische Spannung Isolierung' : 'Electrical stress insulation',
    existsRe: /electrical stress insulation/i,
    key: electricalStressKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Reaktanz' : 'Reactance',
    existsRe: /reactance/i,
    key: reactanceKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Heizzeitkonstante trefoil' : 'Heating time constant trefoil',
    existsRe: /heating time constant.*trefoil/i,
    key: heatingTrefoilKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Heizzeitkonstante flach' : 'Heating time constant flat',
    existsRe: /heating time constant.*flat/i,
    key: heatingFlatKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Flammhemmend' : 'Flame retardant',
    existsRe: /flame retardant/i,
    key: flameKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'CPR-Klasse' : 'CPR class',
    existsRe: /cpr class/i,
    key: cprKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Verpackung' : 'Packaging',
    existsRe: /packaging/i,
    key: packagingKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Biegeradius' : 'Bending radius',
    existsRe: /bending radius/i,
    key: null, // Will be found in row-specific attributes
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Leiterform' : 'Shape of conductor',
    existsRe: /shape of conductor/i,
    key: shapeKey,
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Isolierungsfarbe' : 'Colour of insulation',
    existsRe: /colour of insulation/i,
    key: null, // Will be found in row-specific attributes
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'Mantelfarbe' : 'Colour of sheath',
    existsRe: /colour of sheath/i,
    key: null, // Will be found in row-specific attributes
  });
  
  addConstOrSmallList({
    name: locale === 'de' ? 'RoHS/REACH' : 'RoHS/REACH',
    existsRe: /rohs.*reach/i,
    key: null, // Will be found in row-specific attributes
  });

  product.attributes = attrs;

  if (process.env.PDF_DEBUG_EXCEL === '1') {
    console.log(`[excel] enriched product ${product.id} (${product.slug ?? stripHtml(product.name)}) with ${cfgOptions.length} configurations from excel`);
  }
}

function ensureExcelRowSpecificAttributes(product: ProductData, locale: 'en' | 'de'): void {
  const rows = findExcelRowsForProduct(product);
  if (!rows.length) return;

  const crossSectionAttr =
    findAttr(product, /configuration|konfiguration|aufbau|bezeichnung/i) ||
    findAttr(product, /number of cores and cross-section|querschnitt|cross.?section|mm²|mm2/i);
  if (!crossSectionAttr || !crossSectionAttr.options?.length) return;

  const rowCount = crossSectionAttr.options.length;
  // Only enrich row-specific columns when row counts match (avoid wrong mapping).
  if (rows.length !== rowCount) return;

  const sample = rows[0] || {};

  const keyOuter = guessColumnKey(sample, [/outer diameter \(approx\.?\)/i, /outer diameter of cable/i, /outer diameter\b/i, /diameter over screen/i]);
  const keyWeight = guessColumnKey(sample, [/weight \(approx\.?\)/i, /cable weight/i, /\bweight\b/i]);
  const keyDcRes = guessColumnKey(sample, [/dc resistance at 20/i, /maximum resistance of conductor/i, /resistance conductor/i]);
  const keyCap = guessColumnKey(sample, [/capacitance/i]);
  const keyIndTrefoil = guessColumnKey(sample, [/inductance,?\s*trefoil/i]);
  const keyIndAirFlat = guessColumnKey(sample, [/inductance in air,?\s*flat/i]);
  const keyIndGroundFlat = guessColumnKey(sample, [/inductance in ground,?\s*flat/i]);
  const keyIairTrefoil = guessColumnKey(sample, [/current ratings in air,?\s*trefoil/i]);
  const keyIairFlat = guessColumnKey(sample, [/current ratings in air,?\s*flat/i]);
  const keyIgroundTrefoil = guessColumnKey(sample, [/current ratings in ground,?\s*trefoil/i]);
  const keyIgroundFlat = guessColumnKey(sample, [/current ratings in ground,?\s*flat/i]);
  const keyScCond = guessColumnKey(sample, [/conductor shortcircuit current/i]);
  const keyScScreen = guessColumnKey(sample, [/screen shortcircuit current/i]);
  const keyBend = guessColumnKey(sample, [/bending radius/i, /min\. bending radius/i]);
  
  // Additional row-specific technical data
  const keyConductorDiameter = guessColumnKey(sample, [/conductor diameter/i, /diameter conductor/i]);
  const keyInsulationThickness = guessColumnKey(sample, [/nominal insulation thickness/i, /insulation thickness/i]);
  const keySheathThickness = guessColumnKey(sample, [/nominal sheath thickness/i, /minimum sheath thickness/i, /sheath thickness/i]);
  const keyCapacitance = guessColumnKey(sample, [/capacitance/i]);
  const keyInductanceTrefoil = guessColumnKey(sample, [/inductance.*trefoil/i]);
  const keyInductanceAirFlat = guessColumnKey(sample, [/inductance.*air.*flat/i]);
  const keyInductanceGroundFlat = guessColumnKey(sample, [/inductance.*ground.*flat/i]);
  const keyCurrentAirTrefoil = guessColumnKey(sample, [/current.*air.*trefoil/i]);
  const keyCurrentAirFlat = guessColumnKey(sample, [/current.*air.*flat/i]);
  const keyCurrentGroundTrefoil = guessColumnKey(sample, [/current.*ground.*trefoil/i]);
  const keyCurrentGroundFlat = guessColumnKey(sample, [/current.*ground.*flat/i]);
  const keyHeatingTimeTrefoil = guessColumnKey(sample, [/heating.*time.*trefoil/i]);
  const keyHeatingTimeFlat = guessColumnKey(sample, [/heating.*time.*flat/i]);

  const get = (k: string | null) => rows.map(r => normalizeValue(String(r?.[k ?? ''] ?? '')));
  const withUnit = (vals: string[], unit: string) => vals.map(v => (v && looksNumeric(v) ? `${v} ${unit}` : v));

  // Use labels that are already recognized by the existing PDF regexes.
  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Außen-Ø' : 'Outer diameter',
    options: withUnit(get(keyOuter), 'mm'),
    expectedLen: rowCount,
    existsRe: /outer\s*diameter|außen\s*durchmesser|außen-?ø/i,
  });

  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Gewicht' : 'Weight',
    options: withUnit(get(keyWeight), 'kg/km'),
    expectedLen: rowCount,
    existsRe: /\bweight\b|gewicht/i,
  });

  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'DC-Leiterwiderstand (20C)' : 'DC resistance at 20 C',
    options: withUnit(get(keyDcRes), 'Ohm/km'),
    expectedLen: rowCount,
    existsRe: /dc\s*resistance|max(?:imum)?\s*resistance|resistance\s+conductor|leiterwiderstand/i,
  });

  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Kapazität (ca.)' : 'Capacitance (approx.)',
    options: withUnit(get(keyCap), 'uF/km'),
    expectedLen: rowCount,
    existsRe: /capacitance|kapazit/i,
  });

  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Induktivität, trefoil (ca.)' : 'Inductance, trefoil (approx.)',
    options: withUnit(get(keyIndTrefoil), 'mH/km'),
    expectedLen: rowCount,
    existsRe: /inductance,?\s*trefoil/i,
  });

  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Induktivität in Luft, flach (ca.)' : 'Inductance in air, flat (approx.)',
    options: withUnit(get(keyIndAirFlat), 'mH/km'),
    expectedLen: rowCount,
    existsRe: /inductance\s+in\s+air,?\s*flat/i,
  });

  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Induktivität im Erdreich, flach (ca.)' : 'Inductance in ground, flat (approx.)',
    options: withUnit(get(keyIndGroundFlat), 'mH/km'),
    expectedLen: rowCount,
    existsRe: /inductance\s+in\s+ground,?\s*flat/i,
  });

  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Strombelastbarkeit in Luft, trefoil' : 'Current ratings in air, trefoil',
    options: withUnit(get(keyIairTrefoil), 'A'),
    expectedLen: rowCount,
    existsRe: /current\s+ratings\s+in\s+air,?\s*trefoil/i,
  });

  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Strombelastbarkeit in Luft, flach' : 'Current ratings in air, flat',
    options: withUnit(get(keyIairFlat), 'A'),
    expectedLen: rowCount,
    existsRe: /current\s+ratings\s+in\s+air,?\s*flat/i,
  });

  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Strombelastbarkeit im Erdreich, trefoil' : 'Current ratings in ground, trefoil',
    options: withUnit(get(keyIgroundTrefoil), 'A'),
    expectedLen: rowCount,
    existsRe: /current\s+ratings\s+in\s+ground,?\s*trefoil/i,
  });

  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Strombelastbarkeit im Erdreich, flach' : 'Current ratings in ground, flat',
    options: withUnit(get(keyIgroundFlat), 'A'),
    expectedLen: rowCount,
    existsRe: /current\s+ratings\s+in\s+ground,?\s*flat/i,
  });

  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Kurzschlussstrom Leiter' : 'Conductor shortcircuit current',
    options: withUnit(get(keyScCond), 'kA'),
    expectedLen: rowCount,
    existsRe: /conductor\s+shortcircuit\s+current/i,
  });

  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Kurzschlussstrom Schirm' : 'Screen shortcircuit current',
    options: withUnit(get(keyScScreen), 'kA'),
    expectedLen: rowCount,
    existsRe: /screen\s+shortcircuit\s+current/i,
  });

  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Biegeradius (min.)' : 'Bending radius (min.)',
    options: withUnit(get(keyBend), 'mm'),
    expectedLen: rowCount,
    existsRe: /bending\s*radius|biegeradius/i,
  });
  
  // Additional row-specific technical data
  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Leiterdurchmesser' : 'Conductor diameter',
    options: withUnit(get(keyConductorDiameter), 'mm'),
    expectedLen: rowCount,
    existsRe: /conductor diameter|diameter conductor/i,
  });
  
  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Isolationsdicke' : 'Insulation thickness',
    options: withUnit(get(keyInsulationThickness), 'mm'),
    expectedLen: rowCount,
    existsRe: /insulation thickness|nominal insulation thickness/i,
  });
  
  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Manteldicke' : 'Sheath thickness',
    options: withUnit(get(keySheathThickness), 'mm'),
    expectedLen: rowCount,
    existsRe: /sheath thickness|nominal sheath thickness/i,
  });
  
  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Kapazität' : 'Capacitance',
    options: withUnit(get(keyCapacitance), 'uF/km'),
    expectedLen: rowCount,
    existsRe: /capacitance/i,
  });
  
  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Induktivität trefoil' : 'Inductance trefoil',
    options: withUnit(get(keyInductanceTrefoil), 'mH/km'),
    expectedLen: rowCount,
    existsRe: /inductance.*trefoil/i,
  });
  
  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Induktivität Luft flach' : 'Inductance air flat',
    options: withUnit(get(keyInductanceAirFlat), 'mH/km'),
    expectedLen: rowCount,
    existsRe: /inductance.*air.*flat/i,
  });
  
  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Induktivität Erdreich flach' : 'Inductance ground flat',
    options: withUnit(get(keyInductanceGroundFlat), 'mH/km'),
    expectedLen: rowCount,
    existsRe: /inductance.*ground.*flat/i,
  });
  
  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Strombelastbarkeit Luft trefoil' : 'Current rating air trefoil',
    options: withUnit(get(keyCurrentAirTrefoil), 'A'),
    expectedLen: rowCount,
    existsRe: /current.*air.*trefoil/i,
  });
  
  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Strombelastbarkeit Luft flach' : 'Current rating air flat',
    options: withUnit(get(keyCurrentAirFlat), 'A'),
    expectedLen: rowCount,
    existsRe: /current.*air.*flat/i,
  });
  
  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Strombelastbarkeit Erdreich trefoil' : 'Current rating ground trefoil',
    options: withUnit(get(keyCurrentGroundTrefoil), 'A'),
    expectedLen: rowCount,
    existsRe: /current.*ground.*trefoil/i,
  });
  
  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Strombelastbarkeit Erdreich flach' : 'Current rating ground flat',
    options: withUnit(get(keyCurrentGroundFlat), 'A'),
    expectedLen: rowCount,
    existsRe: /current.*ground.*flat/i,
  });
  
  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Heizzeitkonstante trefoil' : 'Heating time constant trefoil',
    options: withUnit(get(keyHeatingTimeTrefoil), 's'),
    expectedLen: rowCount,
    existsRe: /heating.*time.*trefoil/i,
  });
  
  pushRowAttrIfMissing({
    product,
    name: locale === 'de' ? 'Heizzeitkonstante flach' : 'Heating time constant flat',
    options: withUnit(get(keyHeatingTimeFlat), 's'),
    expectedLen: rowCount,
    existsRe: /heating.*time.*flat/i,
  });
}

function getProductUrl(product: ProductData): string | null {
  if (!product.path) return null;
  return `https://klz-cables.com${product.path}`;
}

function drawKeyValueGrid(args: {
  title: string;
  items: Array<{ label: string; value: string }>;
  newPage: () => number;
  getPage: () => PDFPage;
  page: PDFPage;
  y: number;
  margin: number;
  contentWidth: number;
  contentMinY: number;
  font: PDFFont;
  fontBold: PDFFont;
  navy: ReturnType<typeof rgb>;
  darkGray: ReturnType<typeof rgb>;
  mediumGray: ReturnType<typeof rgb>;
  lightGray?: ReturnType<typeof rgb>;
  almostWhite?: ReturnType<typeof rgb>;
  allowNewPage?: boolean;
  boxed?: boolean;
}): number {
  let { title, items, newPage, getPage, page, y, margin, contentWidth, contentMinY, font, fontBold, navy, darkGray, mediumGray } = args;
  const allowNewPage = args.allowNewPage ?? true;
  const boxed = args.boxed ?? false;

  const lightGray = args.lightGray ?? rgb(0.9020, 0.9137, 0.9294);
  const almostWhite = args.almostWhite ?? rgb(0.9725, 0.9765, 0.9804);

  // Inner layout (boxed vs. plain)
  // Keep a strict spacing system for more professional datasheets.
  const padX = boxed ? 16 : 0;
  const padY = boxed ? 14 : 0;
  const xBase = margin + padX;
  const innerWidth = contentWidth - padX * 2;
  const colGap = 16;
  const colW = (innerWidth - colGap) / 2;
  const rowH = 24;
  const headerH = boxed ? 22 : 0;

  const drawBoxFrame = (boxTopY: number, rowsCount: number) => {
    const boxH = padY + headerH + rowsCount * rowH + padY;
    page = getPage();
    page.drawRectangle({
      x: margin,
      y: boxTopY - boxH,
      width: contentWidth,
      height: boxH,
      borderColor: lightGray,
      borderWidth: 1,
      color: rgb(1, 1, 1),
    });
    // Header band for the title
    page.drawRectangle({
      x: margin,
      y: boxTopY - headerH,
      width: contentWidth,
      height: headerH,
      color: almostWhite,
    });
    return boxH;
  };

  const drawTitle = () => {
    page = getPage();
    if (boxed) {
      // Align title inside the header band.
      page.drawText(title, { x: xBase, y: y - 15, size: 11, font: fontBold, color: navy });
      // Divider line below header band
      page.drawLine({
        start: { x: margin, y: y - headerH },
        end: { x: margin + contentWidth, y: y - headerH },
        thickness: 0.75,
        color: lightGray,
      });
      y -= headerH + padY;
    } else {
      page.drawText(title, { x: margin, y, size: 10, font: fontBold, color: navy });
      y -= 16;
    }
  };

  if (y - 22 < contentMinY) {
    if (!allowNewPage) return contentMinY - 1;
    y = newPage();
  }
  page = getPage();

  // Boxed + multi-page: render separate boxes per page segment.
  if (boxed && allowNewPage) {
    let i = 0;
    while (i < items.length) {
      // Compute how many rows fit in the remaining space.
      const available = y - contentMinY;
      const maxRows = Math.max(1, Math.floor((available - (headerH + padY * 2)) / rowH));
      const maxItems = Math.max(2, maxRows * 2);
      const slice = items.slice(i, i + maxItems);
      const rowsCount = Math.ceil(slice.length / 2);
      const neededH = padY + headerH + rowsCount * rowH + padY;

      if (y - neededH < contentMinY) y = newPage();
      drawBoxFrame(y, rowsCount);

      drawTitle();
      let rowY = y;
      for (let j = 0; j < slice.length; j++) {
        const col = j % 2;
        const x = xBase + col * (colW + colGap);
        const { label, value } = slice[j];
        page.drawText(label, { x, y: rowY, size: 7.5, font: fontBold, color: mediumGray, maxWidth: colW });
        page.drawText(value, { x, y: rowY - 12, size: 9.5, font, color: darkGray, maxWidth: colW });
        if (col === 1) rowY -= rowH;
      }

      y = rowY - rowH - padY;
      i += slice.length;
      if (i < items.length) y = newPage();
    }
    return y;
  }

  // Boxed single-segment (current page) or plain continuation.
  if (boxed && items.length) {
    const rows = Math.ceil(items.length / 2);
    const neededH = padY + headerH + rows * rowH + padY;
    if (y - neededH < contentMinY) {
      if (!allowNewPage) return contentMinY - 1;
      y = newPage();
    }
    drawBoxFrame(y, rows);
  }

  drawTitle();

  let rowY = y;
  for (let i = 0; i < items.length; i++) {
    const col = i % 2;
    const x = xBase + col * (colW + colGap);
    const { label, value } = items[i];

    if (col === 0 && rowY - rowH < contentMinY) {
      if (!allowNewPage) return contentMinY - 1;
      y = newPage();
      page = getPage();
      if (!boxed) drawTitle();
      rowY = y;
    }

    // Don't truncate - allow text to fit naturally
    page.drawText(label, { x, y: rowY, size: 7.5, font: fontBold, color: mediumGray });
    page.drawText(value, { x, y: rowY - 12, size: 9.5, font, color: darkGray });

    if (col === 1) rowY -= rowH;
  }

  return boxed ? rowY - rowH - padY : rowY - rowH;
}

function ensureOutputDir(): void {
  if (!fs.existsSync(CONFIG.outputDir)) {
    fs.mkdirSync(CONFIG.outputDir, { recursive: true });
  }
}

const stripHtml = (html: string): string => {
  if (!html) return '';
  // IMPORTANT: Keep umlauts and common Latin-1 chars (e.g. ü/ö/ä/ß) for DE PDFs.
  // pdf-lib's StandardFonts cover WinAnsi; we only normalize “problematic” typography.
  let text = html.replace(/<[^>]*>/g, '').normalize('NFC');
  text = text
    // whitespace normalization
    .replace(/[\u00A0\u202F]/g, ' ') // nbsp / narrow nbsp
    // typography normalization
    .replace(/[\u2013\u2014]/g, '-') // en/em dash
    .replace(/[\u2018\u2019]/g, "'") // curly single quotes
    .replace(/[\u201C\u201D]/g, '"') // curly double quotes
    .replace(/\u2026/g, '...') // ellipsis
    // symbols that can be missing in some encodings
    .replace(/[\u2022]/g, '·') // bullet
    // math symbols (WinAnsi can't encode these)
    .replace(/[\u2264]/g, '<=') // ≤
    .replace(/[\u2265]/g, '>=') // ≥
    .replace(/[\u2248]/g, '~') // ≈
    // electrical symbols (keep meaning; avoid encoding errors)
    .replace(/[\u03A9\u2126]/g, 'Ohm') // Ω / Ω
    // micro sign / greek mu (WinAnsi can't encode these reliably)
    .replace(/[\u00B5\u03BC]/g, 'u') // µ / μ
    // directional arrows (WinAnsi can't encode these)
    .replace(/[\u2193]/g, 'v') // ↓
    .replace(/[\u2191]/g, '^') // ↑
    // degree symbol (WinAnsi-safe; keep for engineering units like °C)
    .replace(/[\u00B0]/g, '°');

  // Remove control chars, keep all printable unicode.
  text = text.replace(/[\u0000-\u001F\u007F]/g, '');
  return text.replace(/\s+/g, ' ').trim();
};

const getLabels = (locale: 'en' | 'de') => ({
  en: {
    datasheet: 'PRODUCT DATASHEET',
    description: 'DESCRIPTION',
    specs: 'TECHNICAL SPECIFICATIONS',
    crossSection: 'CROSS-SECTION DATA',
    categories: 'CATEGORIES',
    sku: 'SKU',
  },
  de: {
    datasheet: 'PRODUKTDATENBLATT',
    description: 'BESCHREIBUNG',
    specs: 'TECHNISCHE SPEZIFIKATIONEN',
    crossSection: 'QUERSCHNITTSDATEN',
    categories: 'KATEGORIEN',
    sku: 'ARTIKELNUMMER',
  },
})[locale];

const generateFileName = (product: ProductData, locale: 'en' | 'de'): string => {
  const baseName = product.slug || product.translationKey || `product-${product.id}`;
  const cleanSlug = baseName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
  return `${cleanSlug}-${locale}.pdf`;
};

function wrapText(text: string, font: PDFFont, fontSize: number, maxWidth: number): string[] {
  const words = text.split(' ');
  const lines: string[] = [];
  let currentLine = '';

  const isOrphanWord = (w: string) => {
    // Avoid ugly single short words on their own line in DE/EN (e.g. “im”, “in”, “to”).
    // This is a typography/UX improvement for datasheets.
    const s = w.trim();
    return s.length > 0 && s.length <= 2;
  };

  for (let i = 0; i < words.length; i++) {
    const word = words[i];
    const next = i + 1 < words.length ? words[i + 1] : '';
    const testLine = currentLine ? `${currentLine} ${word}` : word;

    if (font.widthOfTextAtSize(testLine, fontSize) <= maxWidth) {
      // Orphan control: if adding the *next* word would overflow, don't end the line with a tiny orphan.
      // Example: "... mechanischen im" + "Belastungen" should become "... mechanischen" / "im Belastungen ...".
      if (currentLine && next && isOrphanWord(word)) {
        const testWithNext = `${testLine} ${next}`;
        if (font.widthOfTextAtSize(testWithNext, fontSize) > maxWidth) {
          lines.push(currentLine);
          currentLine = word;
          continue;
        }
      }

      currentLine = testLine;
    } else {
      if (currentLine) lines.push(currentLine);
      currentLine = word;
    }
  }
  if (currentLine) lines.push(currentLine);
  return lines;
}

function ellipsizeToWidth(text: string, font: PDFFont, fontSize: number, maxWidth: number): string {
  // WinAnsi-safe ellipsis ("...")
  const t = normalizeValue(text);
  if (!t) return '';
  if (font.widthOfTextAtSize(t, fontSize) <= maxWidth) return t;

  const ellipsis = '...';
  const eW = font.widthOfTextAtSize(ellipsis, fontSize);
  if (eW >= maxWidth) return '';

  // Binary search for max prefix that fits.
  let lo = 0;
  let hi = t.length;
  while (lo < hi) {
    const mid = Math.ceil((lo + hi) / 2);
    const s = t.slice(0, mid);
    if (font.widthOfTextAtSize(s, fontSize) + eW <= maxWidth) lo = mid;
    else hi = mid - 1;
  }
  const cut = Math.max(0, lo);
  return `${t.slice(0, cut)}${ellipsis}`;
}

function resolveMediaToLocalPath(urlOrPath: string | null | undefined): string | null {
  if (!urlOrPath) return null;

  // 1) Already public-relative.
  if (urlOrPath.startsWith('/')) return urlOrPath;

  // 2) Some datasets store "media/..." without leading slash.
  if (/^media\//i.test(urlOrPath)) return `/${urlOrPath}`;

  // 3) Asset-map can return a few different shapes; normalize them.
  const mapped = ASSET_MAP[urlOrPath];
  if (mapped) {
    if (mapped.startsWith('/')) return mapped;
    if (/^public\//i.test(mapped)) return `/${mapped.replace(/^public\//i, '')}`;
    if (/^media\//i.test(mapped)) return `/${mapped}`;
    return mapped;
  }

  // 4) Fallback (remote URL or unrecognized local path).
  return urlOrPath;
}

async function fetchBytes(url: string): Promise<Uint8Array> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
  return new Uint8Array(await res.arrayBuffer());
}

async function readBytesFromPublic(localPath: string): Promise<Uint8Array> {
  const abs = path.join(PUBLIC_DIR, localPath.replace(/^\//, ''));
  return new Uint8Array(fs.readFileSync(abs));
}

function transformLogoSvgToPrintBlack(svg: string): string {
  // Our source logo is white-on-transparent (for dark headers). For print (white page), we need dark fills.
  // Keep it simple: replace fill white with KLZ navy.
  return svg
    .replace(/fill\s*:\s*white/gi, 'fill:#0E2A47')
    .replace(/fill\s*=\s*"white"/gi, 'fill="#0E2A47"')
    .replace(/fill\s*=\s*'white'/gi, "fill='#0E2A47'");
}

async function toPngBytes(inputBytes: Uint8Array, inputHint: string): Promise<Uint8Array> {
  // pdf-lib supports PNG/JPG. We normalize everything (webp/svg/jpg/png) to PNG to keep embedding simple.
  const ext = (path.extname(inputHint).toLowerCase() || '').replace('.', '');
  if (ext === 'png') return inputBytes;

  // Special-case the logo SVG to render as dark for print.
  if (ext === 'svg' && /\/media\/logo\.svg$/i.test(inputHint)) {
    const svg = Buffer.from(inputBytes).toString('utf8');
    inputBytes = new Uint8Array(Buffer.from(transformLogoSvgToPrintBlack(svg), 'utf8'));
  }

  const sharp = await getSharp();
  // Preserve alpha where present (some product images are transparent).
  return new Uint8Array(await sharp(Buffer.from(inputBytes)).png().toBuffer());
}

type TableColumn = {
  key?: string;
  label: string;
  get: (rowIndex: number) => string;
};

function buildProductAttrIndex(product: ProductData): Record<string, ProductData['attributes'][number]> {
  const idx: Record<string, ProductData['attributes'][number]> = {};
  for (const a of product.attributes || []) {
    idx[normalizeValue(a.name).toLowerCase()] = a;
  }
  return idx;
}

function getAttrCellValue(attr: ProductData['attributes'][number] | undefined, rowIndex: number, rowCount: number): string {
  if (!attr) return '';
  if (!attr.options || attr.options.length === 0) return '';
  if (rowCount > 0 && attr.options.length === rowCount) return normalizeValue(attr.options[rowIndex]);
  if (attr.options.length === 1) return normalizeValue(attr.options[0]);
  // Unknown mapping: do NOT guess (this was the main source of "wrong" tables).
  return '';
}

function drawTableChunked(args: {
  title: string;
  configRows: string[];
  columns: Array<{ key: string; label: string; get: (rowIndex: number) => string }>;
  firstColLabel?: string;
  dense?: boolean;
  onePage?: boolean;
  cellFormatter?: (value: string, columnKey: string) => string;
  locale: 'en' | 'de';
  newPage: () => number;
  getPage: () => PDFPage;
  page: PDFPage;
  y: number;
  margin: number;
  contentWidth: number;
  contentMinY: number;
  font: PDFFont;
  fontBold: PDFFont;
  navy: ReturnType<typeof rgb>;
  darkGray: ReturnType<typeof rgb>;
  lightGray: ReturnType<typeof rgb>;
  almostWhite: ReturnType<typeof rgb>;
  maxDataColsPerTable: number;
}): number {
  let {
    title,
    configRows,
    columns,
    newPage,
    getPage,
    page,
    y,
    margin,
    contentWidth,
    contentMinY,
    font,
    fontBold,
    navy,
    darkGray,
    lightGray,
    almostWhite,
    maxDataColsPerTable,
  } = args;

  const dense = args.dense ?? false;
  const onePage = args.onePage ?? false;
  const formatCell = args.cellFormatter ?? ((v: string) => v);

  // pdf-lib will wrap text when `maxWidth` is set.
  // For dense technical tables we want *no wrapping* (clip instead), so we replace spaces with NBSP.
  const noWrap = (s: string) => (dense ? String(s).replace(/ /g, '\u00A0') : String(s));

  // pdf-lib does not clip text to maxWidth; it only uses it for line breaking.
  // To prevent header/body text from overlapping into neighboring columns, we manually truncate.
  const truncateToWidth = (text: string, f: PDFFont, size: number, maxW: number): string => {
    // IMPORTANT: do NOT call normalizeValue() here.
    // We may have inserted NBSP to force no-wrapping; normalizeValue() would convert it back to spaces.
    const t = String(text)
      .replace(/[\r\n\t]+/g, ' ')
      .replace(/\s+/g, ' ')
      .trim();
    if (!t) return '';
    if (maxW <= 4) return '';
    if (f.widthOfTextAtSize(t, size) <= maxW) return t;
    const ellipsis = '…';
    const eW = f.widthOfTextAtSize(ellipsis, size);
    if (eW >= maxW) return '';

    // Binary search for max prefix that fits.
    let lo = 0;
    let hi = t.length;
    while (lo < hi) {
      const mid = Math.ceil((lo + hi) / 2);
      const s = t.slice(0, mid);
      if (f.widthOfTextAtSize(s, size) + eW <= maxW) lo = mid;
      else hi = mid - 1;
    }
    const cut = Math.max(0, lo);
    return `${t.slice(0, cut)}${ellipsis}`;
  };

  // Dense table style (more columns, compact typography)
  // IMPORTANT: header labels must stay on a single line (no wrapped/stacked headers).
  const headerH = dense ? 16 : 16;
  let rowH = dense ? 16 : 16;  // Increased to prevent row overlap
  let bodyFontSize = dense ? 6.5 : 8;
  let headerFontSize = dense ? 6.5 : 8;

  const headerFill = dense ? rgb(0.42, 0.44, 0.45) : lightGray;
  const headerText = dense ? rgb(1, 1, 1) : navy;
  let cellPadX = dense ? 4 : 6;

  const headerLabelFor = (col: { label: string; key?: string }): string => {
    // Dense tables: use compact cable industry abbreviations
    const raw = normalizeValue(col.label);
    if (!dense) return raw;

    const b = raw.toLowerCase();
    const key = col.key || '';
    const hasUnit = /\(|\[/.test(raw);

    const labelWithUnit = (abbr: string, defaultUnit: string): string => {
      // Prefer the actual unit we already have in the incoming label (from Excel units row).
      // Example: raw = "DC resistance at 20 °C (Ω/km)" => keep Ω/km.
      const m = raw.match(/[\[(]\s*([^\])]+?)\s*[\])]/);
      const unit = normalizeValue(m?.[1] || '') || defaultUnit;
      // WinAnsi safe: Ω => Ohm, µ => u (handled by stripHtml/normalizeValue)
      const unitSafe = unit.replace(/Ω/gi, 'Ohm').replace(/[\u00B5\u03BC]/g, 'u');
      return `${abbr} [${unitSafe}]`;
    };
    
  // Cable industry standard abbreviations (compact, WinAnsi-safe)
  // Column set:
  //  - Bezeichnung (first column)
  //  - DI, RI, Wi, Ibl, Ibe, Ik, Wm, Rbv, Ø, Fzv, Al, Cu, G
  if (key === 'configuration') return args.locale === 'de' ? 'Bezeichnung' : 'Designation';
  if (key === 'DI' || /diameter\s+over\s+insulation/i.test(b)) return labelWithUnit('DI', 'mm');
  if (key === 'RI' || /dc\s*resistance/i.test(b) || /resistance\s+conductor/i.test(b)) return labelWithUnit('RI', 'Ohm/km');
  if (key === 'Wi' || /insulation\s+thickness/i.test(b) || /nominal\s+insulation\s+thickness/i.test(b)) return labelWithUnit('Wi', 'mm');
  if (key === 'Ibl' || /current\s+ratings\s+in\s+air.*trefoil/i.test(b)) return labelWithUnit('Ibl', 'A');
  if (key === 'Ibe' || /current\s+ratings\s+in\s+ground.*trefoil/i.test(b)) return labelWithUnit('Ibe', 'A');
  if (key === 'Ik_cond' || /conductor.*shortcircuit/i.test(b)) return labelWithUnit('Ik', 'kA');
  if (key === 'Wm' || /sheath\s+thickness/i.test(b) || /minimum\s+sheath\s+thickness/i.test(b)) return labelWithUnit('Wm', 'mm');
  // Rbv can be given in mm or as xD (LV/solar). We keep the unit from the label.
  if (key === 'Rbv' || /bending\s+radius/i.test(b)) return labelWithUnit('Rbv', 'mm');
  if (key === 'Ø' || /outer\s+diameter/i.test(b) || /outer\s+diameter\s+of\s+cable/i.test(b)) return labelWithUnit('Ø', 'mm');
  if (key === 'Fzv' || /pulling\s+force/i.test(b)) return labelWithUnit('Fzv', 'N');
  if (key === 'Al') return 'Al';
  if (key === 'Cu') return 'Cu';
  if (key === 'G' || /\bweight\b/i.test(b) || /cable\s+weight/i.test(b)) return labelWithUnit('G', 'kg/km');
    
    // Cross-section (always needed as first column)
    if (/number of cores and cross-section/i.test(b) || /querschnitt/i.test(b)) {
      return args.locale === 'de' ? 'QS' : 'CS';
    }
    
    // Additional technical columns (WinAnsi-compatible abbreviations)
    if (key === 'cond_diam') return labelWithUnit('D_cond', 'mm');
    if (key === 'D_screen') return labelWithUnit('D_scr', 'mm');
    if (key === 'S_screen') return labelWithUnit('A_scr', 'mm2');
    if (key === 'cap') return labelWithUnit('C', 'uF/km');
    if (key === 'X') return labelWithUnit('X', 'Ohm/km');
    if (key === 'ind_trefoil') return labelWithUnit('L_t', 'mH/km');
    if (key === 'ind_air_flat') return labelWithUnit('L_af', 'mH/km');
    if (key === 'ind_ground_flat') return labelWithUnit('L_gf', 'mH/km');
    if (key === 'cur_air_flat') return labelWithUnit('I_af', 'A');
    if (key === 'cur_ground_flat') return labelWithUnit('I_gf', 'A');
    if (key === 'heat_trefoil') return labelWithUnit('t_th_t', 's');
    if (key === 'heat_flat') return labelWithUnit('t_th_f', 's');
    if (key === 'max_op_temp') return labelWithUnit('T_op', '°C');
    if (key === 'max_sc_temp') return labelWithUnit('T_sc', '°C');
    if (key === 'temp_range') return labelWithUnit('T', '°C');
    if (key === 'min_store_temp') return labelWithUnit('T_st', '°C');
    if (key === 'min_lay_temp') return labelWithUnit('T_lay', '°C');
    if (key === 'test_volt') return labelWithUnit('U_test', 'kV');
    if (key === 'rated_volt') return labelWithUnit('U_0/U', 'kV');
    if (key === 'conductor') return 'Cond';
    if (key === 'insulation') return 'Iso';
    if (key === 'sheath') return 'Sh';
    if (key === 'norm') return 'Norm';
    if (key === 'standard') return 'Std';
    if (key === 'cpr') return 'CPR';
    if (key === 'flame') return 'FR';
    if (key === 'packaging') return 'Pack';
    if (key === 'ce') return 'CE';
    if (key === 'shape') return 'Shape';
    if (key === 'color_ins') return 'C_iso';
    if (key === 'color_sheath') return 'C_sh';
    if (key === 'tape_below') return 'Tape v';
    if (key === 'copper_screen') return 'Cu Scr';
    if (key === 'tape_above') return 'Tape ^';
    if (key === 'al_foil') return 'Al Foil';
    
    // Fallback: keep Excel label (will be truncated)
    return raw;
  };

  // Always include a first column (configuration / cross-section).
  const configCol = {
    key: 'configuration',
    label: args.firstColLabel || (args.locale === 'de' ? 'Konfiguration' : 'Configuration'),
    get: (i: number) => normalizeValue(configRows[i] || ''),
  };

  const chunks: Array<typeof columns> = [];
  for (let i = 0; i < columns.length; i += maxDataColsPerTable) {
    chunks.push(columns.slice(i, i + maxDataColsPerTable));
  }

  for (let ci = 0; ci < Math.max(1, chunks.length); ci++) {
    // Ensure we always draw on the current page reference.
    page = getPage();

    const chunkCols = chunks.length ? chunks[ci] : [];
    // UX: never show chunk fractions like "(1/2)".
    // If we need multiple chunks, we keep the same title and paginate naturally.
    const chunkTitle = title;
    const tableCols: TableColumn[] = [configCol, ...chunkCols];

    // Header labels (may be simplified for dense tables).
    const headerLabels = tableCols.map(c => headerLabelFor({ label: c.label, key: c.key }));

    // Auto-fit column widths to content
    // Calculate required width for each column based on header and sample data
    // For dense tables with many columns, use more generous minimums
    const isDenseManyColumns = dense && tableCols.length >= 12;

    // When rendering many columns, auto-compact typography BEFORE measuring widths.
    // This prevents overflow where the later columns end up drawn off-page.
    if (isDenseManyColumns) {
      bodyFontSize = Math.max(5.3, Math.min(bodyFontSize, 5.8));
      headerFontSize = Math.max(5.1, Math.min(headerFontSize, 5.5));
      rowH = Math.max(9, Math.min(rowH, 10));
      cellPadX = 3;
    }

    const minColWidth = isDenseManyColumns ? 24 : 20; // Minimum width in points
    const maxColWidth = isDenseManyColumns ? 110 : 120; // Cap widths for dense tables
    const colGap = isDenseManyColumns ? 1 : 2; // Gap between columns (smaller for dense)
    
    // Calculate required width for each column
    const requiredWidths = tableCols.map((col, i) => {
      const key = col.key || '';
      const headerLabel = headerLabels[i] || headerLabelFor({ label: col.label, key: col.key });

      // Prefer key-aware constraints so the table is readable on A4.
      // - configuration needs space (long designations)
      // - numeric columns should stay compact
      const isCfg = key === 'configuration';
      // NOTE: keep configuration column capped; otherwise it starves the numeric columns.
      const cfgMaxAbs = dense ? 150 : 180;
      const cfgMaxRel = Math.floor(contentWidth * (dense ? 0.26 : 0.30));
      const cfgMax = Math.max(120, Math.min(cfgMaxAbs, cfgMaxRel));
      const cfgMin = dense ? 90 : 120;

      const minW = isCfg ? cfgMin : minColWidth;
      const maxW = isCfg ? cfgMax : (isDenseManyColumns ? 72 : maxColWidth);

      // Measure header width
      const headerWidth = fontBold.widthOfTextAtSize(headerLabel, headerFontSize);

      // Measure sample data widths using the *rendered* value (apply cell formatter).
      // Using only raw values can under-estimate (e.g. locale decimal, derived values).
      let maxDataWidth = 0;
      const sampleRows = Math.min(10, configRows.length);
      for (let r = 0; r < sampleRows; r++) {
        const rawVal = col.get(r);
        const rendered = formatCell(rawVal, key);
        const dataWidth = font.widthOfTextAtSize(String(rendered), bodyFontSize);
        maxDataWidth = Math.max(maxDataWidth, dataWidth);
      }

      // Take the maximum of header and data
      const padding = isDenseManyColumns ? cellPadX * 1.5 : cellPadX * 2;
      const contentWidthNeeded = Math.max(headerWidth, maxDataWidth) + padding;

      // Clamp to min/max
      return Math.max(minW, Math.min(maxW, contentWidthNeeded));
    });

    // Calculate total required width (including gaps)
    const baseTotal = requiredWidths.reduce((sum, w) => sum + w, 0) + (tableCols.length - 1) * colGap;

    // If we have extra space, give it primarily to the configuration column
    // (best readability gain) and then distribute any remainder proportionally.
    const widthsWithExtra = [...requiredWidths];
    if (baseTotal < contentWidth && tableCols.length > 0) {
      let remaining = contentWidth - baseTotal;

      const cfgIndex = tableCols.findIndex(c => (c.key || '') === 'configuration');
      if (cfgIndex >= 0 && remaining > 0) {
        // Only give the configuration column a controlled share of the remaining space.
        // This keeps numeric columns readable and consistent across tables.
        const cfgMaxAbs = dense ? 150 : 180;
        const cfgMaxRel = Math.floor(contentWidth * (dense ? 0.26 : 0.30));
        const cfgMax = Math.max(120, Math.min(cfgMaxAbs, cfgMaxRel));

        const cfgRemainingCap = Math.min(remaining, Math.floor(remaining * (dense ? 0.35 : 0.45)));
        const add = Math.max(0, Math.min(cfgMax - widthsWithExtra[cfgIndex], cfgRemainingCap));
        widthsWithExtra[cfgIndex] += add;
        remaining -= add;
      }

      if (remaining > 0) {
        const sum = widthsWithExtra.reduce((a, b) => a + b, 0) || 1;
        for (let i = 0; i < widthsWithExtra.length; i++) {
          if (remaining <= 0) break;
          const share = widthsWithExtra[i] / sum;
          const add = Math.min(remaining, remaining * share);
          widthsWithExtra[i] += add;
          remaining -= add;
        }
      }
    }

    const totalRequiredWidth = widthsWithExtra.reduce((sum, w) => sum + w, 0) + (tableCols.length - 1) * colGap;

    // Scale to fit available content width if needed
    // Dense tables MUST fit on the page; allow stronger scaling when needed.
    let scaleFactor = totalRequiredWidth > contentWidth ? contentWidth / totalRequiredWidth : 1;
    if (!isDenseManyColumns) {
      // Keep regular tables from becoming too small.
      scaleFactor = Math.max(scaleFactor, 0.9);
    }

    // Scale widths (gaps are also scaled)
    const widthsPt = widthsWithExtra.map(w => w * scaleFactor);
    const scaledGap = colGap * scaleFactor;

    const ensureSpace = (needed: number) => {
      if (y - needed < contentMinY) y = newPage();
      page = getPage();
    };

    // One-page mode: adapt row height so the full voltage table can fit on the page.
    if (onePage) {
      const rows = configRows.length;
      const available = y - contentMinY - 10 - 12 /*title*/;
      const maxRowH = Math.floor((available - headerH) / Math.max(1, rows));
      rowH = Math.max(8, Math.min(rowH, maxRowH));
      bodyFontSize = Math.max(6, Math.min(bodyFontSize, rowH - 3));
      headerFontSize = Math.max(6, Math.min(headerFontSize, rowH - 3));
    }

    ensureSpace(14 + headerH + rowH * 2);
    if (chunkTitle) {
      page.drawText(chunkTitle, {
        x: margin,
        y,
        size: 10,
        font: fontBold,
        color: navy,
      });
      y -= dense ? 14 : 16;
    }

    // If we are too close to the footer after the title, break before drawing the header.
    if (y - headerH - rowH < contentMinY) {
      y = newPage();
      page = getPage();
      if (chunkTitle) {
        page.drawText(chunkTitle, {
          x: margin,
          y,
          size: 10,
          font: fontBold,
          color: navy,
        });
        y -= 16;
      }
    }

    const drawHeader = () => {
      page = getPage();
      page.drawRectangle({
        x: margin,
        y: y - headerH,
        width: contentWidth,
        height: headerH,
        color: headerFill,
      });

      const headerTextY = y - headerH + Math.floor((headerH - headerFontSize) / 2) + 1;
      let x = margin;
      for (let i = 0; i < tableCols.length; i++) {
        const hl = headerLabels[i] || headerLabelFor({ label: tableCols[i].label, key: tableCols[i].key });
        const colWidth = widthsPt[i];
        const colMaxW = colWidth - cellPadX * 2;
        
        // Overlap guard: always truncate to column width (pdf-lib doesn't clip by itself).
        // Don't truncate headers - use auto-fit widths
        page.drawText(noWrap(String(hl)), {
          x: x + cellPadX,
          y: headerTextY,
          size: headerFontSize,
          font: fontBold,
          color: headerText,
          // DO NOT set maxWidth in dense mode (it triggers wrapping instead of clipping).
          ...(dense ? {} : { maxWidth: colMaxW }),
        });
        x += colWidth + scaledGap;
      }
      y -= headerH;
    };

    drawHeader();

    for (let r = 0; r < configRows.length; r++) {
      if (!onePage && y - rowH < contentMinY) {
        y = newPage();
        page = getPage();
        if (chunkTitle) {
          page.drawText(chunkTitle, {
            x: margin,
            y,
            size: 12,
            font: fontBold,
            color: navy,
          });
          y -= 16;
        }
        drawHeader();
      }

      if (onePage && y - rowH < contentMinY) {
        // In one-page mode we must not paginate. Clip remaining rows.
        break;
      }

      if (r % 2 === 0) {
        page.drawRectangle({
          x: margin,
          y: y - rowH,
          width: contentWidth,
          height: rowH,
          color: almostWhite,
        });
      }

      let x = margin;
      for (let c = 0; c < tableCols.length; c++) {
        const raw = tableCols[c].get(r);
        const txt = formatCell(raw, tableCols[c].key || '');
        const colWidth = widthsPt[c];
        const colMaxW = colWidth - cellPadX * 2;
        // Don't truncate - use auto-fit widths to ensure everything fits
        page.drawText(noWrap(txt), {
          x: x + cellPadX,
          y: y - (rowH - 5),  // Adjusted for new rowH of 16
          size: bodyFontSize,
          font,
          color: darkGray,
          ...(dense ? {} : { maxWidth: colMaxW }),
        });
        x += colWidth + scaledGap;
      }

      y -= rowH;
    }

    y -= dense ? 20 : 24;
  }

  return y;
}

async function loadEmbeddablePng(
  src: string | null | undefined,
): Promise<{ pngBytes: Uint8Array; debugLabel: string } | null> {
  const resolved = resolveMediaToLocalPath(src);
  if (!resolved) return null;

  try {
    // Prefer local files for stability and speed.
    if (resolved.startsWith('/')) {
      const bytes = await readBytesFromPublic(resolved);
      return { pngBytes: await toPngBytes(bytes, resolved), debugLabel: resolved };
    }

    // Remote (fallback)
    const bytes = await fetchBytes(resolved);
    return { pngBytes: await toPngBytes(bytes, resolved), debugLabel: resolved };
  } catch {
    return null;
  }
}

async function loadQrPng(data: string): Promise<{ pngBytes: Uint8Array; debugLabel: string } | null> {
  // External QR generator (no extra dependency). This must stay resilient; if it fails, we fall back to URL text.
  try {
    const safe = encodeURIComponent(data);
    const url = `https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${safe}`;
    const bytes = await fetchBytes(url);
    // Already PNG but normalize anyway.
    return { pngBytes: await toPngBytes(bytes, url), debugLabel: url };
  } catch {
    return null;
  }
}

type SectionDrawContext = {
  pdfDoc: PDFDocument;
  page: PDFPage;
  width: number;
  height: number;
  margin: number;
  contentWidth: number;
  footerY: number;
  contentMinY: number;
  headerDividerY: number;
  colors: {
    navy: ReturnType<typeof rgb>;
    mediumGray: ReturnType<typeof rgb>;
    darkGray: ReturnType<typeof rgb>;
    almostWhite: ReturnType<typeof rgb>;
    lightGray: ReturnType<typeof rgb>;
    headerBg: ReturnType<typeof rgb>;
  };
  fonts: {
    regular: PDFFont;
    bold: PDFFont;
  };
  labels: ReturnType<typeof getLabels>;
  product: ProductData;
  locale: 'en' | 'de';
  logoImage: PDFImage | null;
  qrImage: PDFImage | null;
  qrUrl: string;
};

function drawFooter(ctx: SectionDrawContext): void {
  const { page, width, margin, footerY, fonts, colors, locale } = ctx;

  page.drawLine({
    start: { x: margin, y: footerY + 14 },
    end: { x: width - margin, y: footerY + 14 },
    thickness: 0.75,
    color: colors.lightGray,
  });

  // Left: site URL (always)
  page.drawText(CONFIG.siteUrl, {
    x: margin,
    y: footerY,
    size: 8,
    font: fonts.regular,
    color: colors.mediumGray,
  });

  const dateStr = new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  });

  // Right: date + page number (page number filled in after rendering)
  const rightText = dateStr;
  page.drawText(rightText, {
    x: width - margin - fonts.regular.widthOfTextAtSize(rightText, 8),
    y: footerY,
    size: 8,
    font: fonts.regular,
    color: colors.mediumGray,
  });
}

function stampPageNumbers(pdfDoc: PDFDocument, fonts: { regular: PDFFont }, colors: { mediumGray: ReturnType<typeof rgb> }, margin: number, footerY: number): void {
  const pages = pdfDoc.getPages();
  const total = pages.length;
  for (let i = 0; i < total; i++) {
    const page = pages[i];
    const { width } = page.getSize();
    const text = `${i + 1}/${total}`;
    page.drawText(text, {
      x: width - margin - fonts.regular.widthOfTextAtSize(text, 8),
      y: footerY - 12,
      size: 8,
      font: fonts.regular,
      color: colors.mediumGray,
    });
  }
}

function drawHeader(ctx: SectionDrawContext, yStart: number): number {
  const { page, width, margin, contentWidth, fonts, colors, logoImage, qrImage, qrUrl, labels, product } = ctx;

  // Cable-industry look: calm, engineered header with right-aligned meta.
  // Keep header compact to free vertical space for technical tables.
  const headerH = 52;
  const dividerY = yStart - headerH;
  ctx.headerDividerY = dividerY;

  page.drawRectangle({
    x: 0,
    y: dividerY,
    width,
    height: headerH,
    color: colors.headerBg,
  });

  const qrSize = 36;
  const qrGap = 12;
  const rightReserved = qrImage ? qrSize + qrGap : 0;

  // Left: logo (preferred) or typographic fallback
  if (logoImage) {
    const maxLogoW = 120;
    const maxLogoH = 24;
    const scale = Math.min(maxLogoW / logoImage.width, maxLogoH / logoImage.height);
    const w = logoImage.width * scale;
    const h = logoImage.height * scale;
    const logoY = dividerY + Math.round((headerH - h) / 2);
    page.drawImage(logoImage, {
      x: margin,
      y: logoY,
      width: w,
      height: h,
    });
  } else {
    const baseY = dividerY + 22;
    page.drawText('KLZ', {
      x: margin,
      y: baseY,
      size: 22,
      font: fonts.bold,
      color: colors.navy,
    });
    page.drawText('Cables', {
      x: margin + fonts.bold.widthOfTextAtSize('KLZ', 22) + 4,
      y: baseY + 2,
      size: 10,
      font: fonts.regular,
      color: colors.mediumGray,
    });
  }

  // Right: datasheet meta + QR (if available)
  const metaRightEdge = width - margin - rightReserved;
  const metaTitle = labels.datasheet;
  const metaTitleSize = 9;

  const mtW = fonts.bold.widthOfTextAtSize(metaTitle, metaTitleSize);
  // With SKU removed, vertically center the title within the header block.
  const metaY = dividerY + Math.round(headerH / 2 - metaTitleSize / 2);
  page.drawText(metaTitle, {
    x: metaRightEdge - mtW,
    y: metaY,
    size: metaTitleSize,
    font: fonts.bold,
    color: colors.navy,
  });

  if (qrImage) {
    const qrX = width - margin - qrSize;
    const qrY = dividerY + Math.round((headerH - qrSize) / 2);
    page.drawImage(qrImage, { x: qrX, y: qrY, width: qrSize, height: qrSize });
  } else {
    // If QR generation failed, keep the URL available as a compact line.
    const maxW = 260;
    const urlLines = wrapText(qrUrl, fonts.regular, 8, maxW).slice(0, 1);
    if (urlLines.length) {
      const line = urlLines[0];
      const w = fonts.regular.widthOfTextAtSize(line, 8);
      page.drawText(line, {
        x: width - margin - w,
        y: dividerY + 12,
        size: 8,
        font: fonts.regular,
        color: colors.mediumGray,
      });
    }
  }

  // Divider line
  page.drawLine({
    start: { x: margin, y: dividerY },
    end: { x: margin + contentWidth, y: dividerY },
    thickness: 0.75,
    color: colors.lightGray,
  });

  // Content start: keep breathing room below the header, but use page height efficiently.
  return dividerY - 22;
}

function drawCrossSectionChipsRow(args: {
  title: string;
  configRows: string[];
  locale: 'en' | 'de';
  maxLinesCap?: number;
  getPage: () => PDFPage;
  page: PDFPage;
  y: number;
  margin: number;
  contentWidth: number;
  contentMinY: number;
  font: PDFFont;
  fontBold: PDFFont;
  navy: ReturnType<typeof rgb>;
  darkGray: ReturnType<typeof rgb>;
  mediumGray: ReturnType<typeof rgb>;
  lightGray: ReturnType<typeof rgb>;
  almostWhite: ReturnType<typeof rgb>;
}): number {
  let { title, configRows, locale, getPage, page, y, margin, contentWidth, contentMinY, font, fontBold, navy, darkGray, mediumGray, lightGray, almostWhite } = args;

  // Single-page rule: if we can't fit the block, stop.
  const titleH = 12;
  const summaryH = 12;
  const chipH = 16;
  const lineGap = 8;
  const gapY = 10;
  const minLines = 2;
  const needed = titleH + summaryH + (chipH * minLines) + (lineGap * (minLines - 1)) + gapY;
  if (y - needed < contentMinY) return contentMinY - 1;

  page = getPage();

  // Normalize: keep only cross-section part, de-dupe, sort.
  const itemsRaw = configRows
    .map(r => splitConfig(r).crossSection)
    .map(s => normalizeValue(s))
    .filter(Boolean);

  const seen = new Set<string>();
  const items = itemsRaw.filter(v => (seen.has(v) ? false : (seen.add(v), true)));

  items.sort((a, b) => {
    const pa = parseCoresAndMm2(a);
    const pb = parseCoresAndMm2(b);
    if (pa.cores !== null && pb.cores !== null && pa.cores !== pb.cores) return pa.cores - pb.cores;
    if (pa.mm2 !== null && pb.mm2 !== null && pa.mm2 !== pb.mm2) return pa.mm2 - pb.mm2;
    return a.localeCompare(b);
  });

  const total = items.length;
  const parsed = items.map(parseCoresAndMm2).filter(p => p.cores !== null && p.mm2 !== null) as Array<{ cores: number; mm2: number }>;
  const uniqueCores = Array.from(new Set(parsed.map(p => p.cores))).sort((a, b) => a - b);
  const mm2Vals = parsed.map(p => p.mm2).sort((a, b) => a - b);
  const mm2Min = mm2Vals.length ? mm2Vals[0] : null;
  const mm2Max = mm2Vals.length ? mm2Vals[mm2Vals.length - 1] : null;

  page.drawText(title, { x: margin, y, size: 11, font: fontBold, color: navy });
  y -= titleH;

  const summaryParts: string[] = [];
  summaryParts.push(locale === 'de' ? `Varianten: ${total}` : `Options: ${total}`);
  if (uniqueCores.length) summaryParts.push((locale === 'de' ? 'Adern' : 'Cores') + `: ${uniqueCores.join(', ')}`);
  if (mm2Min !== null && mm2Max !== null) summaryParts.push(`mm²: ${mm2Min}${mm2Max !== mm2Min ? `–${mm2Max}` : ''}`);
  page.drawText(summaryParts.join(' · '), { x: margin, y, size: 8, font, color: mediumGray, maxWidth: contentWidth });
  y -= summaryH;

  // Tags (wrapping). Rectangular, engineered (no playful rounding).
  const padX = 8;
  const chipFontSize = 8;
  const chipGap = 8;
  const chipPadTop = 5;

  const startY = y - chipH; // baseline for first chip row
  const maxLinesAvailable = Math.max(1, Math.floor((startY - contentMinY + lineGap) / (chipH + lineGap)));
  // UX/Content priority: don't let cross-section tags consume the whole sheet.
  // When technical data is dense, we cap this to keep specs visible.
  const maxLines = Math.min(args.maxLinesCap ?? 2, maxLinesAvailable);

  const chipWidth = (text: string) => font.widthOfTextAtSize(text, chipFontSize) + padX * 2;

  type Placement = { text: string; x: number; y: number; w: number; variant: 'normal' | 'more' };

  const layout = (texts: string[], includeMoreChip: boolean, moreText: string): { placements: Placement[]; shown: number } => {
    const placements: Placement[] = [];
    let x = margin;
    let line = 0;
    let cy = startY;

    const advanceLine = () => {
      line += 1;
      if (line >= maxLines) return false;
      x = margin;
      cy -= chipH + lineGap;
      return true;
    };

    const tryPlace = (text: string, variant: 'normal' | 'more'): boolean => {
      const w = chipWidth(text);
      if (w > contentWidth) return false;
      if (x + w > margin + contentWidth) {
        if (!advanceLine()) return false;
      }
      placements.push({ text, x, y: cy, w, variant });
      x += w + chipGap;
      return true;
    };

    let shown = 0;
    for (let i = 0; i < texts.length; i++) {
      if (!tryPlace(texts[i], 'normal')) break;
      shown++;
    }

    if (includeMoreChip) {
      tryPlace(moreText, 'more');
    }
    return { placements, shown };
  };

  // Group by cores: label on the left, mm² tags to the right.
  const byCores = new Map<number, number[]>();
  const other: string[] = [];
  for (const cs of items) {
    const p = parseCoresAndMm2(cs);
    if (p.cores !== null && p.mm2 !== null) {
      const arr = byCores.get(p.cores) ?? [];
      arr.push(p.mm2);
      byCores.set(p.cores, arr);
    } else {
      other.push(cs);
    }
  }

  const coreKeys = Array.from(byCores.keys()).sort((a, b) => a - b);
  for (const k of coreKeys) {
    const uniq = Array.from(new Set(byCores.get(k) ?? [])).sort((a, b) => a - b);
    byCores.set(k, uniq);
  }

  const fmtMm2 = (v: number) => {
    const s = Number.isInteger(v) ? String(v) : String(v).replace(/\.0+$/, '');
    return s;
  };

  // Layout engine with group labels.
  const labelW = 38;
  const placements: Placement[] = [];
  let line = 0;
  let cy = startY;
  let x = margin + labelW;

  const canAdvanceLine = () => line + 1 < maxLines;
  const advanceLine = () => {
    if (!canAdvanceLine()) return false;
    line += 1;
    cy -= chipH + lineGap;
    x = margin + labelW;
    return true;
  };

  const drawGroupLabel = (label: string) => {
    // Draw label on each new line for the group (keeps readability when wrapping).
    page.drawText(label, {
      x: margin,
      y: cy + 4,
      size: 8,
      font: fontBold,
      color: mediumGray,
      maxWidth: labelW - 4,
    });
  };

  const placeChip = (text: string, variant: 'normal' | 'more') => {
    const w = chipWidth(text);
    if (w > contentWidth - labelW) return false;
    if (x + w > margin + contentWidth) {
      if (!advanceLine()) return false;
    }
    placements.push({ text, x, y: cy, w, variant });
    x += w + chipGap;
    return true;
  };

  let truncated = false;
  let renderedCount = 0;
  const totalChips = coreKeys.reduce((sum, k) => sum + (byCores.get(k)?.length ?? 0), 0) + other.length;

  for (const cores of coreKeys) {
    const values = byCores.get(cores) ?? [];
    const label = `${cores}×`;
    // Ensure label is shown at least once per line block.
    drawGroupLabel(label);
    for (const v of values) {
      const ok = placeChip(fmtMm2(v), 'normal');
      if (!ok) {
        truncated = true;
        break;
      }
      renderedCount++;
    }
    if (truncated) break;
    // Add a tiny gap between core groups (only if we have room on the current line)
    x += 4;
    if (x > margin + contentWidth - 20) {
      if (!advanceLine()) {
        // out of vertical space; stop
        truncated = true;
        break;
      }
    }
  }

  if (!truncated && other.length) {
    const label = locale === 'de' ? 'Sonst.' : 'Other';
    drawGroupLabel(label);
    for (const t of other) {
      const ok = placeChip(t, 'normal');
      if (!ok) {
        truncated = true;
        break;
      }
      renderedCount++;
    }
  }

  if (truncated) {
    const remaining = Math.max(0, totalChips - renderedCount);
    const moreText = locale === 'de' ? `+${remaining} weitere` : `+${remaining} more`;
    // Try to place on current line; if not possible, try next line.
    if (!placeChip(moreText, 'more')) {
      if (advanceLine()) {
        placeChip(moreText, 'more');
      }
    }
  }

  // Draw placements
  for (const p of placements) {
    page.drawRectangle({
      x: p.x,
      y: p.y,
      width: p.w,
      height: chipH,
      borderColor: lightGray,
      borderWidth: 1,
      color: rgb(1, 1, 1),
    });
    page.drawText(p.text, {
      x: p.x + padX,
      y: p.y + chipPadTop,
      size: chipFontSize,
      font,
      color: p.variant === 'more' ? navy : darkGray,
      maxWidth: p.w - padX * 2,
    });
  }

  // Return cursor below the last line drawn
  const linesUsed = placements.length ? Math.max(...placements.map(p => Math.round((startY - p.y) / (chipH + lineGap)))) + 1 : 1;
  const bottomY = startY - (linesUsed - 1) * (chipH + lineGap);
  // Consistent section spacing after block.
  // IMPORTANT: never return below contentMinY if we actually rendered,
  // otherwise callers may think it "didn't fit" and draw a fallback on top (duplicate “Options” lines).
  return Math.max(bottomY - 24, contentMinY);
}

function drawCompactList(args: {
  items: string[];
  x: number;
  y: number;
  colW: number;
  cols: number;
  rowH: number;
  maxRows: number;
  page: PDFPage;
  font: PDFFont;
  fontSize: number;
  color: ReturnType<typeof rgb>;
}): number {
  const { items, x, colW, cols, rowH, maxRows, page, font, fontSize, color } = args;
  let y = args.y;
  const shown = items.slice(0, cols * maxRows);
  for (let i = 0; i < shown.length; i++) {
    const col = Math.floor(i / maxRows);
    const row = i % maxRows;
    const ix = x + col * colW;
    const iy = y - row * rowH;
    page.drawText(shown[i], {
      x: ix,
      y: iy,
      size: fontSize,
      font,
      color,
      maxWidth: colW - 6,
    });
  }
  return y - maxRows * rowH;
}

function findAttr(product: ProductData, includes: RegExp): ProductData['attributes'][number] | undefined {
  return product.attributes?.find(a => includes.test(a.name));
}

function normalizeValue(value: string): string {
  return stripHtml(value).replace(/\s+/g, ' ').trim();
}

function summarizeOptions(options: string[] | undefined, maxItems: number = 3): string {
  const vals = (options || []).map(normalizeValue).filter(Boolean);
  if (vals.length === 0) return '';
  const uniq = Array.from(new Set(vals));
  if (uniq.length === 1) return uniq[0];
  if (uniq.length <= maxItems) return uniq.join(' / ');
  // UX: avoid showing internal counts like "+8" in customer-facing PDFs.
  // Indicate truncation with an ellipsis.
  return `${uniq.slice(0, maxItems).join(' / ')} / ...`;
}

function parseNumericOption(value: string): number | null {
  const v = normalizeValue(value).replace(/,/g, '.');
  // First numeric token (works for "12.3", "12.3 mm", "-35", "26.5 kg/km").
  const m = v.match(/-?\d+(?:\.\d+)?/);
  if (!m) return null;
  const n = Number(m[0]);
  return Number.isFinite(n) ? n : null;
}

function formatNumber(n: number): string {
  const s = Number.isInteger(n) ? String(n) : String(n);
  return s.replace(/\.0+$/, '');
}

function summarizeNumericRange(options: string[] | undefined): { ok: boolean; text: string } {
  const vals = (options || []).map(parseNumericOption).filter((n): n is number => n !== null);
  if (vals.length < 3) return { ok: false, text: '' };
  const uniq = Array.from(new Set(vals));
  // If there are only a few distinct values, listing is clearer than a range.
  if (uniq.length < 4) return { ok: false, text: '' };
  uniq.sort((a, b) => a - b);
  const min = uniq[0];
  const max = uniq[uniq.length - 1];
  // UX: don't show internal counts like "n=…" in customer-facing datasheets.
  return { ok: true, text: `${formatNumber(min)}–${formatNumber(max)}` };
}

function summarizeSmartOptions(label: string, options: string[] | undefined): string {
  // Prefer numeric ranges when an attribute has many numeric-ish entries (typical for row-specific data).
  const range = summarizeNumericRange(options);
  if (range.ok) return range.text;
  return summarizeOptions(options, 3);
}

function looksNumeric(value: string): boolean {
  const v = normalizeValue(value).replace(/,/g, '.');
  return /^-?\d+(?:\.\d+)?$/.test(v);
}

function formatMaybeWithUnit(value: string, unit: string): string {
  const v = normalizeValue(value);
  if (!v) return '';
  return looksNumeric(v) ? `${v} ${unit}` : v;
}

function drawRowPreviewTable(args: {
  title: string;
  rows: Array<{ config: string; col1: string; col2: string }>;
  headers: { config: string; col1: string; col2: string };
  getPage: () => PDFPage;
  page: PDFPage;
  y: number;
  margin: number;
  contentWidth: number;
  contentMinY: number;
  font: PDFFont;
  fontBold: PDFFont;
  navy: ReturnType<typeof rgb>;
  darkGray: ReturnType<typeof rgb>;
  lightGray: ReturnType<typeof rgb>;
  almostWhite: ReturnType<typeof rgb>;
}): number {
  let { title, rows, headers, getPage, page, y, margin, contentWidth, contentMinY, font, fontBold, navy, darkGray, lightGray, almostWhite } = args;

  const titleH = 16;
  const headerH = 16;
  const rowH = 13;
  const padAfter = 18;

  // One-page rule: require at least 2 data rows.
  const minNeeded = titleH + headerH + rowH * 2 + padAfter;
  if (y - minNeeded < contentMinY) return contentMinY - 1;

  page = getPage();
  page.drawText(title, { x: margin, y, size: 10, font: fontBold, color: navy });
  y -= titleH;

  // How many rows fit?
  const availableForRows = y - contentMinY - padAfter - headerH;
  const maxRows = Math.max(2, Math.floor(availableForRows / rowH));
  const shown = rows.slice(0, Math.max(0, maxRows));

  const hasCol2 = shown.some(r => Boolean(r.col2));

  // Widths: favor configuration readability.
  const wCfg = 0.46;
  const w1 = hasCol2 ? 0.27 : 0.54;
  const w2 = hasCol2 ? 0.27 : 0;

  const x0 = margin;
  const x1 = margin + contentWidth * wCfg;
  const x2 = margin + contentWidth * (wCfg + w1);

  const drawHeader = () => {
    page = getPage();
    page.drawRectangle({ x: margin, y: y - headerH, width: contentWidth, height: headerH, color: lightGray });
    page.drawText(headers.config, { x: x0 + 6, y: y - 11, size: 8, font: fontBold, color: navy, maxWidth: contentWidth * wCfg - 12 });
    page.drawText(headers.col1, { x: x1 + 6, y: y - 11, size: 8, font: fontBold, color: navy, maxWidth: contentWidth * w1 - 12 });
    if (hasCol2) {
      page.drawText(headers.col2, { x: x2 + 6, y: y - 11, size: 8, font: fontBold, color: navy, maxWidth: contentWidth * w2 - 12 });
    }
    y -= headerH;
  };

  drawHeader();

  for (let i = 0; i < shown.length; i++) {
    if (y - rowH < contentMinY) return contentMinY - 1;
    page = getPage();
    if (i % 2 === 0) {
      page.drawRectangle({ x: margin, y: y - rowH, width: contentWidth, height: rowH, color: almostWhite });
    }
    page.drawText(shown[i].config, { x: x0 + 6, y: y - 10, size: 8, font, color: darkGray, maxWidth: contentWidth * wCfg - 12 });
    page.drawText(shown[i].col1, { x: x1 + 6, y: y - 10, size: 8, font, color: darkGray, maxWidth: contentWidth * w1 - 12 });
    if (hasCol2) {
      page.drawText(shown[i].col2, { x: x2 + 6, y: y - 10, size: 8, font, color: darkGray, maxWidth: contentWidth * w2 - 12 });
    }
    y -= rowH;
  }

  y -= padAfter;
  return y;
}

function splitConfig(config: string): { crossSection: string; voltage: string } {
  const raw = normalizeValue(config);
  const parts = raw.split(/\s*-\s*/);
  if (parts.length >= 2) {
    return { crossSection: parts[0], voltage: parts.slice(1).join(' - ') };
  }
  return { crossSection: raw, voltage: '' };
}

function parseCoresAndMm2(crossSection: string): { cores: number | null; mm2: number | null } {
  const s = normalizeValue(crossSection)
    .replace(/\s+/g, '')
    .replace(/×/g, 'x')
    .replace(/,/g, '.');

  // Typical: 3x1.5, 4x25, 1x70
  const m = s.match(/(\d{1,3})x(\d{1,4}(?:\.\d{1,2})?)/i);
  if (!m) return { cores: null, mm2: null };
  const cores = Number(m[1]);
  const mm2 = Number(m[2]);
  return {
    cores: Number.isFinite(cores) ? cores : null,
    mm2: Number.isFinite(mm2) ? mm2 : null,
  };
}

async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise<Buffer> {
  try {
    const labels = getLabels(locale);
    const pdfDoc = await PDFDocument.create();
    const pageSizePortrait: [number, number] = [595.28, 841.89]; // DIN A4 portrait
    const pageSizeLandscape: [number, number] = [841.89, 595.28]; // DIN A4 landscape
    let page = pdfDoc.addPage(pageSizePortrait);
    const { width, height } = page.getSize();

    // STYLEGUIDE.md colors
    const navy = rgb(0.0549, 0.1647, 0.2784); // #0E2A47
    const mediumGray = rgb(0.4196, 0.4471, 0.5020); // #6B7280
    const darkGray = rgb(0.1216, 0.1608, 0.2); // #1F2933
    const almostWhite = rgb(0.9725, 0.9765, 0.9804); // #F8F9FA
    const lightGray = rgb(0.9020, 0.9137, 0.9294); // #E6E9ED
    const headerBg = rgb(0.965, 0.972, 0.98); // calm, print-friendly tint

    // Small design system: consistent type + spacing for professional datasheets.
    const DS = {
      space: { xs: 4, sm: 8, md: 12, lg: 16, xl: 24 },
      type: { h1: 20, h2: 11, body: 10.5, small: 8 },
      rule: { thin: 0.75 },
    } as const;

    // Line-heights (explicit so vertical rhythm doesn't drift / overlap)
    const LH = {
      h1: 24,
      h2: 16,
      body: 14,
      small: 10,
    } as const;

    const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
    const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);

    // Assets
    // Prefer a raster logo for reliability (sharp SVG support can vary between environments).
    const logoPng = (await loadEmbeddablePng('/media/logo.png')) || (await loadEmbeddablePng('/media/logo.svg'));
    const logoImage = logoPng ? await pdfDoc.embedPng(logoPng.pngBytes) : null;

    // Some products have no product-specific images.
    // Do NOT fall back to a generic/category hero (misleading in datasheets).
    // If missing, we render a neutral placeholder box.
    const heroSrc = product.featuredImage || product.images?.[0] || null;
    const heroPng = heroSrc ? await loadEmbeddablePng(heroSrc) : null;

    const productUrl = getProductUrl(product) || CONFIG.siteUrl;
    const qrPng = await loadQrPng(productUrl);
    const qrImage = qrPng ? await pdfDoc.embedPng(qrPng.pngBytes) : null;

    // Engineered page frame (A4): slightly narrower margins but consistent rhythm.
    const margin = 54;
    const footerY = 54;
    const contentMinY = footerY + 42; // keep clear of footer + page numbers
    const contentWidth = width - 2 * margin;

    const ctx: SectionDrawContext = {
      pdfDoc,
      page,
      width,
      height,
      margin,
      contentWidth,
      footerY,
      contentMinY,
      headerDividerY: 0,
      colors: { navy, mediumGray, darkGray, almostWhite, lightGray, headerBg },
      fonts: { regular: font, bold: fontBold },
      labels,
      product,
      locale,
      logoImage,
      qrImage,
      qrUrl: productUrl,
    };

    const syncCtxForPage = (p: PDFPage) => {
      const sz = p.getSize();
      ctx.page = p;
      ctx.width = sz.width;
      ctx.height = sz.height;
      ctx.contentWidth = sz.width - 2 * ctx.margin;
    };

    const drawPageBackground = (p: PDFPage) => {
      const { width: w, height: h } = p.getSize();
      p.drawRectangle({
        x: 0,
        y: 0,
        width: w,
        height: h,
        color: rgb(1, 1, 1),
      });
    };

    const drawProductNameOnPage = (p: PDFPage, yStart: number): number => {
      const name = stripHtml(product.name);
      const maxW = ctx.contentWidth;
      const line = wrapText(name, fontBold, 12, maxW).slice(0, 1)[0] || name;
      p.drawText(line, {
        x: margin,
        y: yStart,
        size: 12,
        font: fontBold,
        color: navy,
        maxWidth: maxW,
      });
      return yStart - 18;
    };

    // Real multi-page support.
    // Each new page repeats header + footer for print-friendly, consistent scanning.
    const newPage = (opts?: { includeProductName?: boolean; landscape?: boolean }): number => {
      page = pdfDoc.addPage(opts?.landscape ? pageSizeLandscape : pageSizePortrait);
      syncCtxForPage(page);
      drawPageBackground(page);
      drawFooter(ctx);
      let yStart = drawHeader(ctx, ctx.height - ctx.margin);
      if (opts?.includeProductName) yStart = drawProductNameOnPage(page, yStart);
      return yStart;
    };

    const hasSpace = (needed: number) => y - needed >= contentMinY;

    // ---- Layout helpers (eliminate magic numbers; enforce consistent rhythm) ----
    const rule = (gapAbove: number = DS.space.md, gapBelow: number = DS.space.lg) => {
      // One-page rule: if we can't fit a divider with its spacing, do nothing.
      if (!hasSpace(gapAbove + gapBelow + DS.rule.thin)) return;

      y -= gapAbove;
      page.drawLine({
        start: { x: margin, y },
        end: { x: margin + contentWidth, y },
        thickness: DS.rule.thin,
        color: lightGray,
      });
      y -= gapBelow;
    };

    const sectionTitle = (text: string) => {
      // One-page rule: if we can't fit the heading + its gap, do nothing.
      if (!hasSpace(DS.type.h2 + DS.space.md)) return;

      page.drawText(text, {
        x: margin,
        y,
        size: DS.type.h2,
        font: fontBold,
        color: navy,
      });
      // Use a real line-height to avoid title/body overlap.
      y -= LH.h2;
    };

    // Page 1
    syncCtxForPage(page);
    drawPageBackground(page);
    drawFooter(ctx);
    let y = drawHeader(ctx, ctx.height - ctx.margin);

    // === PRODUCT HEADER ===
    const productName = stripHtml(product.name);
    const cats = (product.categories || []).map(c => stripHtml(c.name)).join(' • ');

    // First page: keep the header compact so technical tables start earlier.
    const titleW = contentWidth;
    const titleSize = 18;
    const titleLineH = 21;
    const nameLines = wrapText(productName, fontBold, titleSize, titleW);
    const shownNameLines = nameLines.slice(0, 1);
    for (const line of shownNameLines) {
      if (y - titleLineH < contentMinY) y = newPage();
      page.drawText(line, {
        x: margin,
        y,
        size: titleSize,
        font: fontBold,
        color: navy,
        maxWidth: titleW,
      });
      y -= titleLineH;
    }

    if (cats) {
      if (y - 18 < contentMinY) y = newPage();
      page.drawText(cats, {
        x: margin,
        y,
        size: 10.5,
        font,
        color: mediumGray,
        maxWidth: titleW,
      });
      y -= DS.space.md;
    }

    // Separator after product header
    // No dividing lines on first page (cleaner, more space-efficient).
    // rule(DS.space.xs, DS.space.md);

    // === HERO IMAGE (full width) ===
    // Dense technical products need more room for specs; prioritize content over imagery.
    const hasLotsOfTech = (product.attributes?.length || 0) >= 18;
    let heroH = hasLotsOfTech ? 96 : 120;
    const afterHeroGap = DS.space.lg;
    if (!hasSpace(heroH + afterHeroGap)) {
      // Shrink to remaining space (but keep it usable).
      heroH = Math.max(84, Math.floor(y - contentMinY - afterHeroGap));
    }

    const heroBoxX = margin;
    const heroBoxY = y - heroH;
    page.drawRectangle({
      x: heroBoxX,
      y: heroBoxY,
      width: contentWidth,
      height: heroH,
      // Calm frame; gives images consistent presence even with transparency.
      color: almostWhite,
      borderColor: lightGray,
      borderWidth: 1,
    });

    if (heroPng) {
      const pad = DS.space.md;
      const boxW = contentWidth - pad * 2;
      const boxH = heroH - pad * 2;

      // Fit image into the box without cutting it off (contain).
      // Technical images often must remain fully visible.
      const sharp = await getSharp();
      const contained = await sharp(Buffer.from(heroPng.pngBytes))
        .resize({
          width: 1200,
          height: Math.round((1200 * boxH) / boxW),
          fit: 'contain',
          background: { r: 248, g: 249, b: 250, alpha: 1 },
        })
        .png()
        .toBuffer();
      const heroImage = await pdfDoc.embedPng(contained);

      page.drawImage(heroImage, {
        x: heroBoxX + pad,
        y: heroBoxY + pad,
        width: boxW,
        height: boxH,
      });
    } else {
      page.drawText(locale === 'de' ? 'Kein Bild verfügbar' : 'No image available', {
        x: heroBoxX + 12,
        y: heroBoxY + heroH / 2,
        size: 8,
        font,
        color: mediumGray,
        maxWidth: contentWidth - 24,
      });
    }

    y = heroBoxY - afterHeroGap;

      // === DESCRIPTION (optional) ===
      if (product.shortDescriptionHtml || product.descriptionHtml) {
        const desc = stripHtml(product.shortDescriptionHtml || product.descriptionHtml);
        const descLineH = 14;
        // Keep full length: render all lines, paginate if needed.
        const boxPadX = DS.space.md;
        const boxPadY = DS.space.md;

        sectionTitle(labels.description);

        const maxTextW = contentWidth - boxPadX * 2;
        const descLines = wrapText(desc, font, DS.type.body, maxTextW);

        // Draw as boxed paragraphs, paginating as needed.
        // We keep a single consistent box per page segment.
        let i = 0;
        while (i < descLines.length) {
          // Ensure we have enough space for at least 2 lines.
          const minLines = 2;
          const minBoxH = boxPadY * 2 + descLineH * minLines;
          if (!hasSpace(minBoxH + DS.space.md)) {
            y = newPage({ includeProductName: true });
          }

          // Compute how many lines we can fit on the current page.
          const availableH = y - contentMinY - DS.space.md;
          const maxLinesThisPage = Math.max(minLines, Math.floor((availableH - boxPadY * 2) / descLineH));
          const slice = descLines.slice(i, i + maxLinesThisPage);

          const boxH = boxPadY * 2 + descLineH * slice.length;
          const boxTop = y + DS.space.xs;
          const boxBottom = boxTop - boxH;

          page.drawRectangle({
            x: margin,
            y: boxBottom,
            width: contentWidth,
            height: boxH,
            color: rgb(1, 1, 1),
            borderColor: lightGray,
            borderWidth: 1,
          });

          let ty = boxTop - boxPadY - DS.type.body;
          for (const line of slice) {
            page.drawText(line, {
              x: margin + boxPadX,
              y: ty,
              size: DS.type.body,
              font,
              color: darkGray,
            });
            ty -= descLineH;
          }

          y = boxBottom - DS.space.md;
          i += slice.length;

          // If there is more text, continue on a new page segment (keeps layout stable).
          if (i < descLines.length) {
            y = newPage({ includeProductName: true });
            sectionTitle(labels.description);
          }
        }

        rule(0, DS.space.lg);
      }

    // === EXCEL MODEL ===
    // Priority: render ALL Excel data (technical + per-voltage tables).
    const excelModel = buildExcelModel({ product, locale });

    // Keep the old enrichment as a fallback path only.
    // (Some products may not match Excel keying; then we still have a usable PDF.)
    ensureExcelCrossSectionAttributes(product, locale);
    ensureExcelRowSpecificAttributes(product, locale);

    if (excelModel.ok) {
      const tables = excelModel.voltageTables;
      const hasMultipleVoltages = tables.length > 1;
      
      // TECHNICAL DATA (shared across all cross-sections)
      const techTitle = locale === 'de' ? 'TECHNISCHE DATEN' : 'TECHNICAL DATA';
      const techItems = excelModel.technicalItems;

      // Track if we've rendered any content before the tables
      let hasRenderedContent = false;

      if (techItems.length) {
        y = drawKeyValueGrid({
          title: techTitle,
          items: techItems,
          newPage: () => newPage({ includeProductName: true }),
          getPage: () => page,
          page,
          y,
          margin,
          contentWidth,
          contentMinY,
          font,
          fontBold,
          navy,
          darkGray,
          mediumGray,
          lightGray,
          almostWhite,
          allowNewPage: true,
          boxed: true,
        });
        
        hasRenderedContent = true;
        
        // Add spacing after technical data section before first voltage table
        if (y - 20 >= contentMinY) y -= 20;
      }

      // CROSS-SECTION DATA: one table per voltage rating
      const firstColLabel =
        locale === 'de' ? 'Adern & Querschnitt' : 'Cores & cross-section';

      for (const t of tables) {
        // Maintain a minimum space between tables (even when staying on the same page).
        // This avoids visual collisions between the previous table and the next meta header.
        if (hasRenderedContent && y - 20 >= contentMinY) y -= 20;

        // Check if we need a new page for this voltage table
        // Estimate: meta block (if shown) + table header + at least 3 data rows
        const estimateMetaH = (itemsCount: number) => {
          // Always render a voltage-group header (even for single-voltage products)
          // so all datasheets look consistent.
          const titleH = 14;
          const rowH = 14;
          const cols = 3;
          const rows = Math.max(1, Math.ceil(Math.max(0, itemsCount) / cols));
          return titleH + rows * rowH + 8;
        };
        const minTableH = 16 /*header*/ + 9 * 3 /*3 rows*/ + 10 /*pad*/;
        const minNeeded = estimateMetaH((t.metaItems || []).length) + minTableH;
        if (y - minNeeded < contentMinY) {
          y = newPage({ includeProductName: true, landscape: false });
        }

        // Top meta block: always render per voltage group.
        // This ensures a consistent structure across products (LV/MV/HV) and makes the
        // voltage group visible in the heading.
        y = drawDenseMetaGrid({
          title: `${labels.crossSection} — ${t.voltageLabel}`,
          items: t.metaItems,
          locale,
          newPage: () => newPage({ includeProductName: true, landscape: false }),
          getPage: () => page,
          page,
          y,
          margin,
          contentWidth,
          contentMinY,
          font,
          fontBold,
          navy,
          darkGray,
          mediumGray,
        });

        // Breathing room before the dense table
        y -= 14;

        // Cross-section table: exactly 13 columns as specified
        // Order: DI, RI, Wi, Ibl, Ibe, Ik, Wm, Rbv, Ø, Fzv, Al, Cu, G
        const tableColumns = prioritizeColumnsForDenseTable({ columns: t.columns });

        // Format dense table cells: compact decimals, no units in cells
        const cellFormatter = (value: string, columnKey: string) => {
          return compactNumericForLocale(value, locale);
        };

        // Table title: keep empty because the voltage-group title is already shown in the meta block.
        const tableTitle = '';

        y = drawTableChunked({
          title: tableTitle,
          configRows: t.crossSections,
          columns: tableColumns,
          firstColLabel,
          dense: true,
          onePage: false, // Allow multiple pages for large tables
          cellFormatter,
          locale,
          newPage: () => newPage({ includeProductName: true, landscape: false }),
          getPage: () => page,
          page,
          y,
          margin,
          contentWidth: ctx.contentWidth,
          contentMinY,
          font,
          fontBold,
          navy,
          darkGray,
          lightGray,
          almostWhite,
          maxDataColsPerTable: 10_000, // All columns in one table
        });
        
        hasRenderedContent = true;
      }
    } else {
      // Fallback (non-Excel products): keep existing behavior minimal
      const note = locale === 'de'
        ? 'Hinweis: Für dieses Produkt liegen derzeit keine Excel-Daten vor.'
        : 'Note: No Excel data is available for this product yet.';
      y = drawKeyValueGrid({
        title: locale === 'de' ? 'TECHNISCHE DATEN' : 'TECHNICAL DATA',
        items: [{ label: locale === 'de' ? 'Quelle' : 'Source', value: note }],
        newPage: () => newPage({ includeProductName: true }),
        getPage: () => page,
        page,
        y,
        margin,
        contentWidth,
        contentMinY,
        font,
        fontBold,
        navy,
        darkGray,
        mediumGray,
        lightGray,
        almostWhite,
        allowNewPage: true,
        boxed: true,
      });
    }

    // Add page numbers after all pages are created.
    stampPageNumbers(pdfDoc, { regular: font }, { mediumGray }, margin, footerY);

    const pdfBytes = await pdfDoc.save();
    return Buffer.from(pdfBytes);

  } catch (error: any) {
    throw new Error(`Failed to generate PDF for product ${product.id} (${locale}): ${error.message}`);
  }
}

async function processChunk(products: ProductData[], chunkIndex: number, totalChunks: number): Promise<void> {
  console.log(`\nProcessing chunk ${chunkIndex + 1}/${totalChunks} (${products.length} products)...`);

  for (const product of products) {
    try {
      const locale = product.locale || 'en';
      const buffer = await generatePDF(product, locale);
      const fileName = generateFileName(product, locale);
      fs.writeFileSync(path.join(CONFIG.outputDir, fileName), buffer);
      console.log(`✓ ${locale.toUpperCase()}: ${fileName}`);
      await new Promise(resolve => setTimeout(resolve, 50));
    } catch (error) {
      console.error(`✗ Failed to process product ${product.id}:`, error);
    }
  }
}

async function readProductsStream(): Promise<ProductData[]> {
  console.log('Reading products.json...');
  return new Promise((resolve, reject) => {
    const stream = fs.createReadStream(CONFIG.productsFile, { encoding: 'utf8' });
    let data = '';
    stream.on('data', (chunk) => { data += chunk; });
    stream.on('end', () => {
      try {
        const products = JSON.parse(data);
        console.log(`Loaded ${products.length} products`);
        resolve(products);
      } catch (error) {
        reject(new Error(`Failed to parse JSON: ${error}`));
      }
    });
    stream.on('error', (error) => reject(new Error(`Failed to read file: ${error}`)));
  });
}

async function processProductsInChunks(): Promise<void> {
  console.log('Starting PDF generation - Industrial engineering documentation style');
  ensureOutputDir();

  try {
    const allProducts = await readProductsStream();
    if (allProducts.length === 0) {
      console.log('No products found');
      return;
    }

    // Dev convenience: generate only one locale / one product subset.
    // IMPORTANT: apply filters BEFORE PDF_LIMIT so the limit works within the filtered set.
    let products = allProducts;

    const onlyLocale = normalizeValue(String(process.env.PDF_LOCALE || '')).toLowerCase();
    if (onlyLocale === 'de' || onlyLocale === 'en') {
      products = products.filter(p => (p.locale || 'en') === onlyLocale);
    }

    const match = normalizeValue(String(process.env.PDF_MATCH || '')).toLowerCase();
    if (match) {
      products = products.filter(p => {
        const hay = [p.slug, p.translationKey, p.sku, stripHtml(p.name)]
          .filter(Boolean)
          .join(' ')
          .toLowerCase();
        return hay.includes(match);
      });
    }

    // Optional dev convenience: limit how many PDFs we render (useful for design iteration).
    // Default behavior remains unchanged.
    const limit = Number(process.env.PDF_LIMIT || '0');
    products = Number.isFinite(limit) && limit > 0 ? products.slice(0, limit) : products;

    const enProducts = products.filter(p => (p.locale || 'en') === 'en');
    const deProducts = products.filter(p => (p.locale || 'en') === 'de');
    console.log(`Found ${enProducts.length} EN + ${deProducts.length} DE products`);

    const totalChunks = Math.ceil(products.length / CONFIG.chunkSize);
    for (let i = 0; i < totalChunks; i++) {
      const chunk = products.slice(i * CONFIG.chunkSize, (i + 1) * CONFIG.chunkSize);
      await processChunk(chunk, i, totalChunks);
    }

    console.log('\n✅ PDF generation completed!');
    console.log(`Generated ${enProducts.length} EN + ${deProducts.length} DE PDFs`);
    console.log(`Output: ${CONFIG.outputDir}`);

  } catch (error) {
    console.error('❌ Error:', error);
    throw error;
  }
}

async function main(): Promise<void> {
  const start = Date.now();
  try {
    await processProductsInChunks();
    console.log(`\nTime: ${((Date.now() - start) / 1000).toFixed(2)}s`);
  } catch (error) {
    console.error('Fatal error:', error);
    process.exit(1);
  }
}

main().catch(console.error);

export { main as generatePDFDatasheets };
