/**
 * Export Finanzplan to Excel with back-formulas.
 *
 * Generates one .xlsx per scenario. Editable inputs become raw values;
 * computed cells become live Excel formulas that reference the inputs
 * across sheets, so editing an input recalculates downstream values.
 *
 * Run: npx tsx scripts/export-finanzplan-excel.ts
 */

import { Pool } from 'pg'
import ExcelJS from 'exceljs'
import * as path from 'path'

const CONN = process.env.PG_CONN
if (!CONN) {
  console.error('PG_CONN environment variable is required (e.g. postgresql://user:pass@host:port/breakpilot_db)')
  process.exit(1)
}

const SCENARIOS: { id: string; slug: string }[] = [
  { id: 'c0000000-0000-0000-0000-000000000200', slug: 'Wandeldarlehen-200k' },
  { id: 'd0000000-0000-0000-0000-000000000201', slug: 'Wandeldarlehen-Bear' },
  { id: 'd0000000-0000-0000-0000-000000000202', slug: 'Wandeldarlehen-Bull' },
  { id: 'd0000000-0000-0000-0000-000000000300', slug: '1Mio-Euro-Base' },
  { id: 'd0000000-0000-0000-0000-000000000301', slug: '1Mio-Euro-Bear' },
  { id: 'd0000000-0000-0000-0000-000000000302', slug: '1Mio-Euro-Bull' },
]

const MONTHS = 60
const START_YEAR = 2026
const FOUNDING_M = 8 // Aug 2026
const FIRST_M = FOUNDING_M // drop Jan..Jul 2026 from the workbook
const VISIBLE_MONTHS = MONTHS - FIRST_M + 1 // 53

// Number format that displays zero as blank
const NUMFMT = '#,##0;-#,##0;""'

// Sheet name registry — full German names with umlauts.
const SHEET = {
  Dashboard: 'Dashboard',
  Kunden: 'Kunden',
  Umsatz: 'Umsatzerlöse',
  Personal: 'Personalkosten',
  Invest: 'Investitionen',
  Material: 'Materialaufwand',
  Betrieb: 'Betriebliche Aufwendungen',
  Liquid: 'Liquidität',
  GuV: 'GuV',
  Formulas: 'Formelübersicht',
} as const

// In Excel formulas, sheet names with spaces, periods or umlauts must be wrapped in single quotes.
function S(name: string): string {
  return /[\s.\-äöüÄÖÜß]/.test(name) ? `'${name.replace(/'/g, "''")}'` : name
}

// --- column helpers ---
function col(n: number): string {
  // 1 -> A, 26 -> Z, 27 -> AA
  let s = ''
  while (n > 0) {
    const r = (n - 1) % 26
    s = String.fromCharCode(65 + r) + s
    n = Math.floor((n - 1) / 26)
  }
  return s
}

// monthIdx FIRST_M..MONTHS -> Excel column letter (B..BC). A is label column.
const monthCol = (m: number) => col(m - FIRST_M + 2)

// Iterate visible months (FIRST_M..MONTHS)
function* visibleMonths(): Generator<number> {
  for (let m = FIRST_M; m <= MONTHS; m++) yield m
}

function monthToDate(m: number): Date {
  const y = START_YEAR + Math.floor((m - 1) / 12)
  const mo = ((m - 1) % 12) + 1
  return new Date(Date.UTC(y, mo - 1, 1))
}

function dateToMonth(d: Date): number {
  return (d.getUTCFullYear() - START_YEAR) * 12 + (d.getUTCMonth() + 1)
}

// --- types ---
type Row = Record<string, unknown> & { id: number; sort_order: number }

interface ScenarioData {
  scenario: Row
  kunden: Row[]
  umsatz: Row[]
  material: Row[]
  personal: Row[]
  betrieb: Row[]
  invest: Row[]
  sonst: Row[]
  liquid: Row[]
  guv: Row[]
}

async function loadScenario(pool: Pool, id: string): Promise<ScenarioData> {
  const [scen, kunden, umsatz, material, personal, betrieb, invest, sonst, liquid, guv] = await Promise.all([
    pool.query('SELECT * FROM fp_scenarios WHERE id=$1', [id]),
    pool.query('SELECT * FROM fp_kunden WHERE scenario_id=$1 ORDER BY sort_order', [id]),
    pool.query('SELECT * FROM fp_umsatzerloese WHERE scenario_id=$1 ORDER BY sort_order', [id]),
    pool.query('SELECT * FROM fp_materialaufwand WHERE scenario_id=$1 ORDER BY sort_order', [id]),
    pool.query('SELECT * FROM fp_personalkosten WHERE scenario_id=$1 ORDER BY sort_order', [id]),
    pool.query('SELECT * FROM fp_betriebliche_aufwendungen WHERE scenario_id=$1 ORDER BY sort_order, id', [id]),
    pool.query('SELECT * FROM fp_investitionen WHERE scenario_id=$1 ORDER BY sort_order', [id]),
    pool.query('SELECT * FROM fp_sonst_ertraege WHERE scenario_id=$1 ORDER BY sort_order', [id]),
    pool.query('SELECT * FROM fp_liquiditaet WHERE scenario_id=$1 ORDER BY sort_order, id', [id]),
    pool.query('SELECT * FROM fp_guv WHERE scenario_id=$1 ORDER BY sort_order, id', [id]),
  ])
  return {
    scenario: scen.rows[0],
    kunden: kunden.rows,
    umsatz: umsatz.rows,
    material: material.rows,
    personal: personal.rows,
    betrieb: betrieb.rows,
    invest: invest.rows,
    sonst: sonst.rows,
    liquid: liquid.rows,
    guv: guv.rows,
  }
}

// Returns m1..m60 array of values from a JSONB values field
function vals(row: any, field = 'values'): number[] {
  const v = (row[field] || {}) as Record<string, number>
  const out = new Array<number>(MONTHS)
  for (let m = 1; m <= MONTHS; m++) out[m - 1] = Number(v[`m${m}`] || 0)
  return out
}

// Write the three header rows (Year / Month-num / Month-name) on a worksheet starting col B.
// We avoid storing Date objects (timezone hazards) and use numeric Year+Month for formulas.
function writeMonthHeader(ws: ExcelJS.Worksheet): void {
  ws.getCell('A1').value = 'Year'
  ws.getCell('A2').value = 'Month'
  ws.getCell('A3').value = 'Label'
  for (const m of visibleMonths()) {
    const c = monthCol(m)
    const d = monthToDate(m)
    ws.getCell(`${c}1`).value = d.getUTCFullYear()
    ws.getCell(`${c}2`).value = d.getUTCMonth() + 1
    ws.getCell(`${c}3`).value =
      ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getUTCMonth()] +
      ' ' + d.getUTCFullYear()
  }
  ws.getRow(1).font = { bold: true }
  ws.getRow(2).font = { bold: true }
  ws.getRow(3).font = { bold: true }
  ws.getColumn(1).width = 38
  for (let i = 0; i < VISIBLE_MONTHS; i++) ws.getColumn(i + 2).width = 10

  // Freeze panes: keep label column + headers visible
  ws.views = [{ state: 'frozen', xSplit: 1, ySplit: 3 }]
}

// Apply numFmt to all month columns on a row (the data row)
function fmtMonthRow(ws: ExcelJS.Worksheet, r: number, fmt = NUMFMT): void {
  for (const m of visibleMonths()) ws.getCell(`${monthCol(m)}${r}`).numFmt = fmt
}

// Apply NUMFMT to all numeric data cells on a worksheet (skipping header rows 1-3)
function applyNumFmtToSheet(ws: ExcelJS.Worksheet, headerRows = 3): void {
  ws.eachRow({ includeEmpty: false }, (row, rNum) => {
    if (rNum <= headerRows) return
    row.eachCell({ includeEmpty: false }, (cell, cNum) => {
      if (cNum < 2) return
      const v = cell.value
      if (typeof v === 'number' || (typeof v === 'object' && v !== null && 'formula' in (v as any))) {
        cell.numFmt = NUMFMT
      }
    })
  })
}

// Parse a YYYY-MM-DD string (or Date) into { year, month, day } with no timezone shift.
// node-postgres parses date columns as JS Dates constructed in local time, so use local accessors.
function parseYMD(d: unknown): { y: number; m: number; day: number } | null {
  if (!d) return null
  if (d instanceof Date) {
    return { y: d.getFullYear(), m: d.getMonth() + 1, day: d.getDate() }
  }
  const s = String(d)
  const match = s.match(/^(\d{4})-(\d{2})-(\d{2})/)
  if (!match) return null
  return { y: Number(match[1]), m: Number(match[2]), day: Number(match[3]) }
}

// Write the 5-year header for GuV sheet
function writeYearHeader(ws: ExcelJS.Worksheet): void {
  ws.getCell('A1').value = 'Position'
  for (let y = 0; y < 5; y++) {
    const c = col(y + 2)
    ws.getCell(`${c}1`).value = `${START_YEAR + y}`
  }
  ws.getRow(1).font = { bold: true }
  ws.getColumn(1).width = 38
  for (let y = 0; y < 5; y++) ws.getColumn(y + 2).width = 14
}

// =====================================================================
// SHEETS
// =====================================================================

interface SheetRefs {
  kunden: Map<string, number>            // row_label_with_segment -> excel row
  umsatzByLabel: Map<string, number>     // section+row_label -> excel row
  materialByLabel: Map<string, number>
  personalInputRow: Map<number, number>  // person id -> excel row (input block)
  personalBruttoRow: Map<number, number> // person id -> excel row (Brutto monthly)
  personalSozialRow: Map<number, number>
  personalTotalRow: Map<number, number>
  personalSummary: { brutto: number; sozial: number; total: number; headcount: number; founderHc: number }
  investInputRow: Map<number, number>
  investInvestRow: Map<number, number>
  investAfaRow: Map<number, number>
  investTotals: { invest: number; afa: number }
  betriebByLabel: Map<string, number>
  sonstByIdx: Map<number, number>
  sonstSumGesamt: number
  liquidByLabel: Map<string, number>
  guvByLabel: Map<string, number>
}

function buildKunden(wb: ExcelJS.Workbook, data: ScenarioData, refs: SheetRefs): void {
  const ws = wb.addWorksheet(SHEET.Kunden)
  writeMonthHeader(ws)

  // Group kunden by segment_name and row_index. "Gesamt" rows compute formulas; others raw.
  const segRows = data.kunden.filter(r => (r as any).segment_name !== 'Gesamt')
  const gesamtRows = data.kunden.filter(r => (r as any).segment_name === 'Gesamt')

  let r = 4
  for (const row of segRows) {
    const label = `${(row as any).segment_name} — ${(row as any).row_label}`
    ws.getCell(`A${r}`).value = label
    refs.kunden.set((row as any).row_label, r)
    const v = vals(row)
    for (let m = FIRST_M; m <= MONTHS; m++) {
      ws.getCell(`${monthCol(m)}${r}`).value = v[m - 1]
    }
    r++
  }

  r += 1
  for (const row of gesamtRows) {
    const label = (row as any).row_label as string
    ws.getCell(`A${r}`).value = label
    ws.getCell(`A${r}`).font = { bold: true }
    // Match: "Neukunden gesamt" sums all "Neukunden (...)" rows
    const baseLabel = label.replace(' gesamt', '')
    const sumRows: number[] = []
    for (const seg of segRows) {
      const sl = (seg as any).row_label as string
      if (sl.startsWith(baseLabel + ' ')) {
        const er = refs.kunden.get(sl)
        if (er) sumRows.push(er)
      }
    }
    refs.kunden.set(label, r)
    for (let m = FIRST_M; m <= MONTHS; m++) {
      const c = monthCol(m)
      const parts = sumRows.map(er => `${c}${er}`).join('+')
      ws.getCell(`${c}${r}`).value = { formula: parts || '0' }
    }
    r++
  }
}

function buildUmsatz(wb: ExcelJS.Workbook, data: ScenarioData, refs: SheetRefs): void {
  const ws = wb.addWorksheet(SHEET.Umsatz)
  writeMonthHeader(ws)

  // Determine row positions by sort_order
  let r = 4
  const rowsByLabel = new Map<string, number>()
  for (const row of data.umsatz) {
    const label = (row as any).row_label as string
    rowsByLabel.set(label, r)
    refs.umsatzByLabel.set(label, r)
    ws.getCell(`A${r}`).value = label
    if ((row as any).section === 'revenue' && !(row as any).is_editable) ws.getCell(`A${r}`).font = { bold: true }
    r++
  }

  // tier extraction
  const tierOf = (l: string) => {
    const m = l.match(/\(([^)]+)\)/)
    return m ? m[1] : l
  }

  for (const row of data.umsatz) {
    const label = (row as any).row_label as string
    const section = (row as any).section as string
    const editable = (row as any).is_editable as boolean
    const rr = rowsByLabel.get(label)!
    if (section === 'price' || (section === 'quantity' /* not editable but no source we can recompute */) || (section === 'revenue' && editable)) {
      // raw values
      const v = vals(row)
      for (let m = FIRST_M; m <= MONTHS; m++) ws.getCell(`${monthCol(m)}${rr}`).value = v[m - 1]
    } else if (section === 'revenue' && !editable && label !== 'GESAMTUMSATZ') {
      // Umsatz (X) = price_row * quantity_row for same tier
      const tier = tierOf(label)
      const priceRow = data.umsatz.find(x => (x as any).section === 'price' && tierOf((x as any).row_label) === tier)
      const qtyRow = data.umsatz.find(x => (x as any).section === 'quantity' && tierOf((x as any).row_label) === tier)
      const prR = priceRow ? rowsByLabel.get((priceRow as any).row_label) : undefined
      const qR = qtyRow ? rowsByLabel.get((qtyRow as any).row_label) : undefined
      for (let m = FIRST_M; m <= MONTHS; m++) {
        const c = monthCol(m)
        if (prR && qR) ws.getCell(`${c}${rr}`).value = { formula: `${c}${prR}*${c}${qR}` }
        else ws.getCell(`${c}${rr}`).value = vals(row)[m - 1]
      }
    } else if (label === 'GESAMTUMSATZ') {
      // SUM all revenue rows except itself
      const revRows = data.umsatz
        .filter(x => (x as any).section === 'revenue' && (x as any).row_label !== 'GESAMTUMSATZ')
        .map(x => rowsByLabel.get((x as any).row_label)!)
      for (let m = FIRST_M; m <= MONTHS; m++) {
        const c = monthCol(m)
        ws.getCell(`${c}${rr}`).value = { formula: revRows.map(er => `${c}${er}`).join('+') }
      }
    }
  }
}

function buildMaterial(wb: ExcelJS.Workbook, data: ScenarioData, refs: SheetRefs): void {
  const ws = wb.addWorksheet(SHEET.Material)
  writeMonthHeader(ws)

  const costRows = data.material.filter(r => (r as any).section === 'cost')
  let r = 4
  const labelToRow = new Map<string, number>()
  for (const row of costRows) {
    const label = (row as any).row_label as string
    labelToRow.set(label, r)
    refs.materialByLabel.set(label, r)
    ws.getCell(`A${r}`).value = label
    if (label === 'SUMME') ws.getCell(`A${r}`).font = { bold: true }
    r++
  }

  const sumRow = labelToRow.get('SUMME')

  for (const row of costRows) {
    const label = (row as any).row_label as string
    const rr = labelToRow.get(label)!
    if (label === 'SUMME') {
      // SUM all other cost rows
      const ids = [...labelToRow.entries()].filter(([k]) => k !== 'SUMME').map(([, er]) => er)
      for (let m = FIRST_M; m <= MONTHS; m++) {
        const c = monthCol(m)
        ws.getCell(`${c}${rr}`).value = { formula: ids.map(er => `${c}${er}`).join('+') }
      }
    } else if (label.includes('Cloud-Hosting')) {
      // = MAX(0, ${S(SHEET.Kunden)}!Bestandskunden_gesamt[m] - 10) * 100 + 1500, only from m=FOUNDING_M onwards
      const bestRow = refs.kunden.get('Bestandskunden gesamt')
      for (let m = FIRST_M; m <= MONTHS; m++) {
        const c = monthCol(m)
        if (m < FOUNDING_M || !bestRow) {
          ws.getCell(`${c}${rr}`).value = 0
        } else {
          ws.getCell(`${c}${rr}`).value = { formula: `MAX(0,${S(SHEET.Kunden)}!${c}${bestRow}-10)*100+1500` }
        }
      }
    } else {
      const v = vals(row)
      for (let m = FIRST_M; m <= MONTHS; m++) ws.getCell(`${monthCol(m)}${rr}`).value = v[m - 1]
    }
  }
}

function buildPersonal(wb: ExcelJS.Workbook, data: ScenarioData, refs: SheetRefs): void {
  const ws = wb.addWorksheet(SHEET.Personal)
  writeMonthHeader(ws)

  // Input block at top (cols A..H)
  // A: Person Nr, B: Name, C: Position, D: Brutto, E: Raise%, F: Sozial%, G: Start, H: End
  // But col A is used for the month-header row labels too. So we use a different layout:
  //   Row 5: Input headers
  //   Row 6..6+N-1: input rows (A: Nr, B: Name, ..., G: Start, H: End)
  // Then leave a gap and write Brutto/Sozial/Total monthly blocks where col A=label and col B..BI=m1..m60.

  ws.getCell('A5').value = 'Inputs'
  ws.getCell('A5').font = { bold: true }
  // Inputs: A=Nr, B=Name, C=Position, D=Brutto, E=Raise%, F=Sozial%, G=StartYear, H=StartMonth, I=EndYear, J=EndMonth
  const inHdr = ['Nr', 'Name', 'Position', 'Brutto/Monat', 'Raise %/Yr', 'AG-Sozial %', 'Start-Jahr', 'Start-Monat', 'End-Jahr', 'End-Monat']
  inHdr.forEach((h, i) => {
    ws.getCell(`${col(i + 1)}6`).value = h
    ws.getCell(`${col(i + 1)}6`).font = { bold: true }
  })

  const personStart = 7
  data.personal.forEach((row, i) => {
    const rr = personStart + i
    refs.personalInputRow.set((row as any).id, rr)
    ws.getCell(`A${rr}`).value = (row as any).person_nr || ''
    ws.getCell(`B${rr}`).value = (row as any).person_name
    ws.getCell(`C${rr}`).value = (row as any).position || ''
    ws.getCell(`D${rr}`).value = Number((row as any).brutto_monthly || 0)
    ws.getCell(`E${rr}`).value = Number((row as any).annual_raise_pct || 0)
    ws.getCell(`F${rr}`).value = Number((row as any).ag_sozial_pct || 0)
    const sd = parseYMD((row as any).start_date)
    const ed = parseYMD((row as any).end_date)
    ws.getCell(`G${rr}`).value = sd ? sd.y : ''
    ws.getCell(`H${rr}`).value = sd ? sd.m : ''
    ws.getCell(`I${rr}`).value = ed ? ed.y : ''
    ws.getCell(`J${rr}`).value = ed ? ed.m : ''
  })

  // Block: Brutto monthly per person
  const bruttoStart = personStart + data.personal.length + 2
  ws.getCell(`A${bruttoStart - 1}`).value = 'Brutto monatlich'
  ws.getCell(`A${bruttoStart - 1}`).font = { bold: true }
  data.personal.forEach((row, i) => {
    const rr = bruttoStart + i
    const inR = refs.personalInputRow.get((row as any).id)!
    refs.personalBruttoRow.set((row as any).id, rr)
    ws.getCell(`A${rr}`).value = `${(row as any).person_name} — Brutto`
    for (let m = FIRST_M; m <= MONTHS; m++) {
      const c = monthCol(m)
      // Compare monthIndex = year*12 + month. Active if startKey<=cur and (endKey is blank or cur<=endKey).
      // Brutto = ROUND(brutto * (1+raise/100)^(curYear - startYear), 0)
      const cur = `${c}$1*12+${c}$2`
      const startKey = `$G$${inR}*12+$H$${inR}`
      const endKey = `$I$${inR}*12+$J$${inR}`
      const active = `AND(${cur}>=${startKey},OR($I$${inR}="",${cur}<=${endKey}))`
      const expo = `${c}$1-$G$${inR}`
      const f = `IF(${active},ROUND($D$${inR}*(1+$E$${inR}/100)^(${expo}),0),0)`
      ws.getCell(`${c}${rr}`).value = { formula: f }
    }
  })

  // Sum Brutto
  const sumBruttoRow = bruttoStart + data.personal.length + 1
  ws.getCell(`A${sumBruttoRow}`).value = 'TOTAL Brutto'
  ws.getCell(`A${sumBruttoRow}`).font = { bold: true }
  for (let m = FIRST_M; m <= MONTHS; m++) {
    const c = monthCol(m)
    const refs1 = data.personal.map(p => `${c}${refs.personalBruttoRow.get((p as any).id)!}`).join('+')
    ws.getCell(`${c}${sumBruttoRow}`).value = { formula: refs1 || '0' }
  }
  refs.personalSummary.brutto = sumBruttoRow

  // Block: Sozial monthly
  const sozialStart = sumBruttoRow + 2
  ws.getCell(`A${sozialStart - 1}`).value = 'AG-Sozialversicherung monatlich'
  ws.getCell(`A${sozialStart - 1}`).font = { bold: true }
  data.personal.forEach((row, i) => {
    const rr = sozialStart + i
    const inR = refs.personalInputRow.get((row as any).id)!
    const bR = refs.personalBruttoRow.get((row as any).id)!
    refs.personalSozialRow.set((row as any).id, rr)
    ws.getCell(`A${rr}`).value = `${(row as any).person_name} — Sozial`
    for (let m = FIRST_M; m <= MONTHS; m++) {
      const c = monthCol(m)
      ws.getCell(`${c}${rr}`).value = { formula: `ROUND(${c}${bR}*$F$${inR}/100,0)` }
    }
  })
  const sumSozialRow = sozialStart + data.personal.length + 1
  ws.getCell(`A${sumSozialRow}`).value = 'TOTAL Sozial'
  ws.getCell(`A${sumSozialRow}`).font = { bold: true }
  for (let m = FIRST_M; m <= MONTHS; m++) {
    const c = monthCol(m)
    const refs1 = data.personal.map(p => `${c}${refs.personalSozialRow.get((p as any).id)!}`).join('+')
    ws.getCell(`${c}${sumSozialRow}`).value = { formula: refs1 || '0' }
  }
  refs.personalSummary.sozial = sumSozialRow

  // Block: Total = Brutto + Sozial
  const totalStart = sumSozialRow + 2
  ws.getCell(`A${totalStart - 1}`).value = 'Total pro Person monatlich'
  ws.getCell(`A${totalStart - 1}`).font = { bold: true }
  data.personal.forEach((row, i) => {
    const rr = totalStart + i
    const bR = refs.personalBruttoRow.get((row as any).id)!
    const sR = refs.personalSozialRow.get((row as any).id)!
    refs.personalTotalRow.set((row as any).id, rr)
    ws.getCell(`A${rr}`).value = `${(row as any).person_name} — Total`
    for (let m = FIRST_M; m <= MONTHS; m++) {
      const c = monthCol(m)
      ws.getCell(`${c}${rr}`).value = { formula: `${c}${bR}+${c}${sR}` }
    }
  })
  const sumTotalRow = totalStart + data.personal.length + 1
  ws.getCell(`A${sumTotalRow}`).value = 'TOTAL Personalkosten'
  ws.getCell(`A${sumTotalRow}`).font = { bold: true }
  for (let m = FIRST_M; m <= MONTHS; m++) {
    const c = monthCol(m)
    const refs1 = data.personal.map(p => `${c}${refs.personalTotalRow.get((p as any).id)!}`).join('+')
    ws.getCell(`${c}${sumTotalRow}`).value = { formula: refs1 || '0' }
  }
  refs.personalSummary.total = sumTotalRow

  // Headcount (= count of persons with brutto>0 in that month)
  const hcRow = sumTotalRow + 2
  ws.getCell(`A${hcRow}`).value = 'Headcount'
  ws.getCell(`A${hcRow}`).font = { bold: true }
  const firstBr = refs.personalBruttoRow.get((data.personal[0] as any).id)!
  const lastBr = refs.personalBruttoRow.get((data.personal[data.personal.length - 1] as any).id)!
  for (let m = FIRST_M; m <= MONTHS; m++) {
    const c = monthCol(m)
    ws.getCell(`${c}${hcRow}`).value = { formula: `COUNTIF(${c}${firstBr}:${c}${lastBr},">0")` }
  }
  refs.personalSummary.headcount = hcRow

  // Headcount minus founders (2). Used by some opex formulas.
  const hcMinusRow = hcRow + 1
  ws.getCell(`A${hcMinusRow}`).value = 'Headcount (ohne 2 Gruender)'
  ws.getCell(`A${hcMinusRow}`).font = { bold: true }
  for (let m = FIRST_M; m <= MONTHS; m++) {
    const c = monthCol(m)
    ws.getCell(`${c}${hcMinusRow}`).value = { formula: `MAX(0,${c}${hcRow}-2)` }
  }
  refs.personalSummary.founderHc = hcMinusRow
}

function buildInvest(wb: ExcelJS.Workbook, data: ScenarioData, refs: SheetRefs): void {
  const ws = wb.addWorksheet(SHEET.Invest)
  writeMonthHeader(ws)

  // Input block: A=Position, B=Kategorie, C=Betrag, D=Anschaffungs-Jahr, E=Anschaffungs-Monat, F=AfA Jahre
  ws.getCell('A5').value = 'Inputs'
  ws.getCell('A5').font = { bold: true }
  const inHdr = ['Position', 'Kategorie', 'Betrag', 'Anschaff-Jahr', 'Anschaff-Monat', 'AfA Jahre']
  inHdr.forEach((h, i) => {
    ws.getCell(`${col(i + 1)}6`).value = h
    ws.getCell(`${col(i + 1)}6`).font = { bold: true }
  })

  const itemStart = 7
  data.invest.forEach((row, i) => {
    const rr = itemStart + i
    refs.investInputRow.set((row as any).id, rr)
    ws.getCell(`A${rr}`).value = (row as any).item_name
    ws.getCell(`B${rr}`).value = (row as any).category || ''
    ws.getCell(`C${rr}`).value = Number((row as any).purchase_amount || 0)
    const pd = parseYMD((row as any).purchase_date)
    ws.getCell(`D${rr}`).value = pd ? pd.y : ''
    ws.getCell(`E${rr}`).value = pd ? pd.m : ''
    ws.getCell(`F${rr}`).value = (row as any).afa_years ?? ''
  })

  // Block: Investitionsausgaben per item
  const invStart = itemStart + data.invest.length + 2
  ws.getCell(`A${invStart - 1}`).value = 'Investitionsausgaben'
  ws.getCell(`A${invStart - 1}`).font = { bold: true }
  data.invest.forEach((row, i) => {
    const rr = invStart + i
    const inR = refs.investInputRow.get((row as any).id)!
    refs.investInvestRow.set((row as any).id, rr)
    ws.getCell(`A${rr}`).value = `${(row as any).item_name} — Ausgabe`
    for (let m = FIRST_M; m <= MONTHS; m++) {
      const c = monthCol(m)
      // Outlay only in purchase month: when cur_year==purchase_year AND cur_month==purchase_month
      const f = `IF(AND(${c}$1=$D$${inR},${c}$2=$E$${inR}),$C$${inR},0)`
      ws.getCell(`${c}${rr}`).value = { formula: f }
    }
  })
  const sumInvestRow = invStart + data.invest.length + 1
  ws.getCell(`A${sumInvestRow}`).value = 'TOTAL Investitionsausgaben'
  ws.getCell(`A${sumInvestRow}`).font = { bold: true }
  for (let m = FIRST_M; m <= MONTHS; m++) {
    const c = monthCol(m)
    const parts = data.invest.map(it => `${c}${refs.investInvestRow.get((it as any).id)!}`).join('+')
    ws.getCell(`${c}${sumInvestRow}`).value = { formula: parts || '0' }
  }
  refs.investTotals.invest = sumInvestRow

  // Block: AfA per item
  const afaStart = sumInvestRow + 2
  ws.getCell(`A${afaStart - 1}`).value = 'Abschreibungen (AfA)'
  ws.getCell(`A${afaStart - 1}`).font = { bold: true }
  data.invest.forEach((row, i) => {
    const rr = afaStart + i
    const inR = refs.investInputRow.get((row as any).id)!
    refs.investAfaRow.set((row as any).id, rr)
    ws.getCell(`A${rr}`).value = `${(row as any).item_name} — AfA`
    const hasYears = (row as any).afa_years != null && Number((row as any).afa_years) > 0
    for (let m = FIRST_M; m <= MONTHS; m++) {
      const c = monthCol(m)
      // Inputs: D=purchaseYear, E=purchaseMonth, F=afa_years
      // monthIndex = year*12 + month
      const cur = `${c}$1*12+${c}$2`
      const startKey = `$D$${inR}*12+$E$${inR}`
      // end_exclusive = startKey + F*12
      const endKey = `$D$${inR}*12+$E$${inR}+$F$${inR}*12`
      let f: string
      if (hasYears) {
        f = `IF(AND(${cur}>=${startKey},${cur}<${endKey}),ROUND($C$${inR}/($F$${inR}*12),0),0)`
      } else {
        // GWG: full in purchase month
        f = `IF(AND(${c}$1=$D$${inR},${c}$2=$E$${inR}),$C$${inR},0)`
      }
      ws.getCell(`${c}${rr}`).value = { formula: f }
    }
  })
  const sumAfaRow = afaStart + data.invest.length + 1
  ws.getCell(`A${sumAfaRow}`).value = 'TOTAL AfA'
  ws.getCell(`A${sumAfaRow}`).font = { bold: true }
  for (let m = FIRST_M; m <= MONTHS; m++) {
    const c = monthCol(m)
    const parts = data.invest.map(it => `${c}${refs.investAfaRow.get((it as any).id)!}`).join('+')
    ws.getCell(`${c}${sumAfaRow}`).value = { formula: parts || '0' }
  }
  refs.investTotals.afa = sumAfaRow
}

function buildSonst(wb: ExcelJS.Workbook, data: ScenarioData, refs: SheetRefs): void {
  const ws = wb.addWorksheet('SonstErtraege')
  writeMonthHeader(ws)

  // Group by category. is_sum_row=true means subtotal across that category's detail rows.
  let r = 4
  const rowsByLabel = new Map<string, number>()
  // Pre-pass: assign rows
  for (const row of data.sonst) {
    rowsByLabel.set(`${(row as any).category}|${(row as any).row_label}|${(row as any).row_index}`, r)
    refs.sonstByIdx.set((row as any).row_index, r)
    ws.getCell(`A${r}`).value = `${(row as any).category} — ${(row as any).row_label}`
    if ((row as any).is_sum_row) ws.getCell(`A${r}`).font = { bold: true }
    r++
  }

  for (const row of data.sonst) {
    const key = `${(row as any).category}|${(row as any).row_label}|${(row as any).row_index}`
    const rr = rowsByLabel.get(key)!
    if ((row as any).row_label === 'GESAMTUMSATZ') {
      // Sum of all category-sum rows
      const sumIds = data.sonst
        .filter(x => (x as any).is_sum_row && (x as any).row_label !== 'GESAMTUMSATZ')
        .map(x => rowsByLabel.get(`${(x as any).category}|${(x as any).row_label}|${(x as any).row_index}`)!)
      for (let m = FIRST_M; m <= MONTHS; m++) {
        const c = monthCol(m)
        ws.getCell(`${c}${rr}`).value = { formula: sumIds.length ? sumIds.map(er => `${c}${er}`).join('+') : '0' }
      }
      refs.sonstSumGesamt = rr
    } else if ((row as any).is_sum_row) {
      // Sum of detail rows in same category
      const detailIds = data.sonst
        .filter(x => (x as any).category === (row as any).category && !(x as any).is_sum_row)
        .map(x => rowsByLabel.get(`${(x as any).category}|${(x as any).row_label}|${(x as any).row_index}`)!)
      for (let m = FIRST_M; m <= MONTHS; m++) {
        const c = monthCol(m)
        ws.getCell(`${c}${rr}`).value = { formula: detailIds.length ? detailIds.map(er => `${c}${er}`).join('+') : '0' }
      }
    } else {
      const v = vals(row)
      for (let m = FIRST_M; m <= MONTHS; m++) ws.getCell(`${monthCol(m)}${rr}`).value = v[m - 1]
    }
  }
}

function buildBetrieb(wb: ExcelJS.Workbook, data: ScenarioData, refs: SheetRefs): void {
  const ws = wb.addWorksheet(SHEET.Betrieb)
  writeMonthHeader(ws)

  // Excel column for the date row (row 3): column letter c
  // Engine-defined formula rows (label -> per-cell formula factory)
  // Per-unit formulas (apply only m >= FOUNDING_M, else 0)
  const perUnitMap: Record<string, { perUnit: number; source: 'hc' | 'hcMinusFounders' | 'bestand' }> = {
    'Fort-/Weiterbildungskosten (F)': { perUnit: 300, source: 'hcMinusFounders' },
    'Reisekosten (F)': { perUnit: 75, source: 'hc' },
    'Bewirtungskosten (F)': { perUnit: 50, source: 'bestand' },
    'Internet/Mobilfunk (F)': { perUnit: 50, source: 'hc' },
  }

  let r = 4
  for (const row of data.betrieb) {
    refs.betriebByLabel.set((row as any).row_label as string, r)
    ws.getCell(`A${r}`).value = `${(row as any).category} — ${(row as any).row_label}`
    if ((row as any).is_sum_row) ws.getCell(`A${r}`).font = { bold: true }
    r++
  }

  // First pass: simple rows
  for (const row of data.betrieb) {
    const label = (row as any).row_label as string
    const rr = refs.betriebByLabel.get(label)!
    const isSum = (row as any).is_sum_row as boolean
    const category = (row as any).category as string

    if (label === 'Personalkosten' && category === 'personal') {
      for (let m = FIRST_M; m <= MONTHS; m++) {
        const c = monthCol(m)
        ws.getCell(`${c}${rr}`).value = { formula: `${S(SHEET.Personal)}!${c}${refs.personalSummary.total}` }
      }
      continue
    }
    if (label === 'Abschreibungen' && category === 'abschreibungen') {
      for (let m = FIRST_M; m <= MONTHS; m++) {
        const c = monthCol(m)
        ws.getCell(`${c}${rr}`).value = { formula: `${S(SHEET.Invest)}!${c}${refs.investTotals.afa}` }
      }
      continue
    }
    if (perUnitMap[label]) {
      const { perUnit, source } = perUnitMap[label]
      for (let m = FIRST_M; m <= MONTHS; m++) {
        const c = monthCol(m)
        if (m < FOUNDING_M) { ws.getCell(`${c}${rr}`).value = 0; continue }
        let src = ''
        if (source === 'hc') src = `${S(SHEET.Personal)}!${c}${refs.personalSummary.headcount}`
        else if (source === 'hcMinusFounders') src = `${S(SHEET.Personal)}!${c}${refs.personalSummary.founderHc}`
        else src = `${S(SHEET.Kunden)}!${c}${refs.kunden.get('Bestandskunden gesamt')!}`
        ws.getCell(`${c}${rr}`).value = { formula: `${src}*${perUnit}` }
      }
      continue
    }
    if (label.includes('Berufsgenossenschaft')) {
      for (let m = FIRST_M; m <= MONTHS; m++) {
        const c = monthCol(m)
        if (m < FOUNDING_M) { ws.getCell(`${c}${rr}`).value = 0; continue }
        ws.getCell(`${c}${rr}`).value = { formula: `ROUND(${S(SHEET.Personal)}!${c}${refs.personalSummary.brutto}*0.005,0)` }
      }
      continue
    }
    if (label.includes('Allgemeine Marketingkosten')) {
      const umsRow = refs.umsatzByLabel.get('GESAMTUMSATZ')!
      for (let m = FIRST_M; m <= MONTHS; m++) {
        const c = monthCol(m)
        if (m < FOUNDING_M) { ws.getCell(`${c}${rr}`).value = 0; continue }
        const rate = m <= 36 ? 0.08 : 0.10
        ws.getCell(`${c}${rr}`).value = { formula: `ROUND(${S(SHEET.Umsatz)}!${c}${umsRow}*${rate},0)` }
      }
      continue
    }
    if (isSum) continue // handled below
    // Default: raw values
    const v = vals(row)
    for (let m = FIRST_M; m <= MONTHS; m++) ws.getCell(`${monthCol(m)}${rr}`).value = v[m - 1]
  }

  // Second pass: category sums + sum sonstige + gesamtkosten + gewerbesteuer
  // Determine for each row: which non-tax-opex rows belong to "rest of opex" for Gewerbesteuer
  const nonTaxOpexRows = data.betrieb.filter(r => {
    const cat = (r as any).category as string
    if (cat === 'steuern' || cat === 'personal' || cat === 'abschreibungen') return false
    if ((r as any).is_sum_row) return false
    const lbl = (r as any).row_label as string
    if (lbl.includes('Summe') || lbl.includes('SUMME')) return false
    return true
  })

  for (const row of data.betrieb) {
    const label = (row as any).row_label as string
    const rr = refs.betriebByLabel.get(label)!
    const isSum = (row as any).is_sum_row as boolean
    const category = (row as any).category as string

    if (label.includes('Gewerbesteuer')) {
      const umsRow = refs.umsatzByLabel.get('GESAMTUMSATZ')!
      const matSum = refs.materialByLabel.get('SUMME')!
      const persTotal = refs.personalSummary.total
      const afaTotal = refs.investTotals.afa
      for (let m = FIRST_M; m <= MONTHS; m++) {
        const c = monthCol(m)
        if (m < FOUNDING_M) { ws.getCell(`${c}${rr}`).value = 0; continue }
        const opexParts = nonTaxOpexRows
          .map(or => `${c}${refs.betriebByLabel.get((or as any).row_label as string)!}`)
          .join('+')
        const profit = `${S(SHEET.Umsatz)}!${c}${umsRow}-${S(SHEET.Material)}!${c}${matSum}-${S(SHEET.Personal)}!${c}${persTotal}-${S(SHEET.Invest)}!${c}${afaTotal}-(${opexParts})`
        ws.getCell(`${c}${rr}`).value = { formula: `IF((${profit})>0,ROUND((${profit})*0.1225,0),0)` }
      }
      continue
    }
    if (!isSum) continue

    if (label.includes('Summe sonstige')) {
      // Sum: all betrieb rows except personal/abschreibungen, sum rows, gesamtkosten
      const detailIds = data.betrieb.filter(x => {
        const xl = (x as any).row_label as string
        if (xl === 'Personalkosten' || xl === 'Abschreibungen') return false
        if (xl.includes('Summe sonstige') || xl.includes('Gesamtkosten') || xl.includes('SUMME Betriebliche')) return false
        if ((x as any).is_sum_row) return false
        if ((x as any).category === 'personal' || (x as any).category === 'abschreibungen') return false
        return true
      }).map(x => refs.betriebByLabel.get((x as any).row_label as string)!)
      for (let m = FIRST_M; m <= MONTHS; m++) {
        const c = monthCol(m)
        ws.getCell(`${c}${rr}`).value = { formula: detailIds.length ? detailIds.map(er => `${c}${er}`).join('+') : '0' }
      }
      continue
    }
    if (label.includes('Gesamtkosten') || label.includes('SUMME Betriebliche')) {
      const persR = refs.betriebByLabel.get('Personalkosten')!
      const abrR = refs.betriebByLabel.get('Abschreibungen')!
      const sonstSum = [...refs.betriebByLabel.entries()].find(([k]) => k.includes('Summe sonstige'))?.[1]
      for (let m = FIRST_M; m <= MONTHS; m++) {
        const c = monthCol(m)
        ws.getCell(`${c}${rr}`).value = { formula: `${c}${persR}+${c}${abrR}+${c}${sonstSum}` }
      }
      continue
    }
    // Category sum (steuern, versicherungen, besondere, marketing, sonstige, fahrzeug)
    const detailIds = data.betrieb.filter(x => (x as any).category === category && !(x as any).is_sum_row)
      .map(x => refs.betriebByLabel.get((x as any).row_label as string)!)
    for (let m = FIRST_M; m <= MONTHS; m++) {
      const c = monthCol(m)
      ws.getCell(`${c}${rr}`).value = { formula: detailIds.length ? detailIds.map(er => `${c}${er}`).join('+') : '0' }
    }
  }
}

function buildLiquid(wb: ExcelJS.Workbook, data: ScenarioData, refs: SheetRefs): void {
  const ws = wb.addWorksheet(SHEET.Liquid)
  writeMonthHeader(ws)

  let r = 4
  for (const row of data.liquid) {
    refs.liquidByLabel.set((row as any).row_label as string, r)
    ws.getCell(`A${r}`).value = `${(row as any).row_type} — ${(row as any).row_label}`
    if (!(row as any).is_editable && (row as any).row_type !== 'einzahlung') ws.getCell(`A${r}`).font = { bold: true }
    r++
  }

  // Pass 1: linked rows + editable raw rows
  for (const row of data.liquid) {
    const label = (row as any).row_label as string
    const rType = (row as any).row_type as string
    const editable = (row as any).is_editable as boolean
    const rr = refs.liquidByLabel.get(label)!

    const setFormulaAll = (fn: (c: string) => string) => {
      for (let m = FIRST_M; m <= MONTHS; m++) {
        const c = monthCol(m)
        ws.getCell(`${c}${rr}`).value = { formula: fn(c) }
      }
    }

    if (label === 'Umsatzerlöse') {
      const umsRow = refs.umsatzByLabel.get('GESAMTUMSATZ')!
      setFormulaAll(c => `${S(SHEET.Umsatz)}!${c}${umsRow}`)
      continue
    }
    if (label === 'Sonst. betriebl. Erträge') {
      // SonstErtraege sheet was dropped (empty in DB). Write 0 inline.
      for (const m of visibleMonths()) ws.getCell(`${monthCol(m)}${rr}`).value = 0
      continue
    }
    if (label === 'Materialaufwand') {
      const matR = refs.materialByLabel.get('SUMME')!
      setFormulaAll(c => `${S(SHEET.Material)}!${c}${matR}`)
      continue
    }
    if (label === 'Personalkosten') {
      setFormulaAll(c => `${S(SHEET.Personal)}!${c}${refs.personalSummary.total}`)
      continue
    }
    if (label === 'Sonstige Kosten') {
      const sonstSum = [...refs.betriebByLabel.entries()].find(([k]) => k.includes('Summe sonstige'))?.[1]
      if (sonstSum) {
        setFormulaAll(c => `${S(SHEET.Betrieb)}!${c}${sonstSum}`)
        continue
      }
    }
    if (label === 'Investitionen' && rType === 'ueberschuss') {
      setFormulaAll(c => `${S(SHEET.Invest)}!${c}${refs.investTotals.invest}`)
      continue
    }
    // Editable rows: write raw values (stored in DB)
    if (editable) {
      const v = vals(row)
      for (let m = FIRST_M; m <= MONTHS; m++) ws.getCell(`${monthCol(m)}${rr}`).value = v[m - 1]
      continue
    }
    // For Gewerbesteuer / Körperschaftsteuer in Liquiditaet — they reference GuV (1/12 of annual)
    // Write raw values for now (they have circular dependency we don't want in Excel)
    const v = vals(row)
    for (let m = FIRST_M; m <= MONTHS; m++) ws.getCell(`${monthCol(m)}${rr}`).value = v[m - 1]
  }

  // Pass 2: aggregation rows (Summe ERTRÄGE, AUSZAHLUNGEN, UEBERSCHUSS chain, Kontostand, LIQUIDITAT)
  const sumEin = refs.liquidByLabel.get('Summe ERTRÄGE') || refs.liquidByLabel.get('Summe EINZAHLUNGEN')
  const sumAus = refs.liquidByLabel.get('Summe AUSZAHLUNGEN')
  const uvi = refs.liquidByLabel.get('ÜBERSCHUSS VOR INVESTITIONEN') || refs.liquidByLabel.get('UEBERSCHUSS VOR INVESTITIONEN')
  const uve = refs.liquidByLabel.get('ÜBERSCHUSS VOR ENTNAHMEN') || refs.liquidByLabel.get('UEBERSCHUSS VOR ENTNAHMEN')
  const ueb = refs.liquidByLabel.get('ÜBERSCHUSS') || refs.liquidByLabel.get('UEBERSCHUSS')
  const liqInvest = refs.liquidByLabel.get('Investitionen')
  const entnahmen = refs.liquidByLabel.get('Kapitalentnahmen/Ausschüttungen') || refs.liquidByLabel.get('Kapitalentnahmen/Ausschuettungen')

  // Find kontostand (no LIQUIDIT in label) and liquiditaet (with LIQUIDIT)
  const kontostandRow = data.liquid.find(r => (r as any).row_type === 'kontostand' && !((r as any).row_label as string).includes('LIQUIDIT'))
  const liqRow = data.liquid.find(r => (r as any).row_type === 'kontostand' && ((r as any).row_label as string).includes('LIQUIDIT'))
  const konto = kontostandRow ? refs.liquidByLabel.get((kontostandRow as any).row_label) : undefined
  const liquidit = liqRow ? refs.liquidByLabel.get((liqRow as any).row_label) : undefined

  const einzIds = data.liquid.filter(x => (x as any).row_type === 'einzahlung' && (x as any).row_label !== 'Summe ERTRÄGE' && (x as any).row_label !== 'Summe EINZAHLUNGEN')
    .map(x => refs.liquidByLabel.get((x as any).row_label as string)!)
  const auszIds = data.liquid.filter(x => (x as any).row_type === 'auszahlung' && (x as any).row_label !== 'Summe AUSZAHLUNGEN')
    .map(x => refs.liquidByLabel.get((x as any).row_label as string)!)

  for (let m = FIRST_M; m <= MONTHS; m++) {
    const c = monthCol(m)
    if (sumEin) ws.getCell(`${c}${sumEin}`).value = { formula: einzIds.map(er => `${c}${er}`).join('+') || '0' }
    if (sumAus) ws.getCell(`${c}${sumAus}`).value = { formula: auszIds.map(er => `${c}${er}`).join('+') || '0' }
    if (uvi && sumEin && sumAus) ws.getCell(`${c}${uvi}`).value = { formula: `${c}${sumEin}-${c}${sumAus}` }
    if (uve && uvi && liqInvest) ws.getCell(`${c}${uve}`).value = { formula: `${c}${uvi}-${c}${liqInvest}` }
    if (ueb && uve && entnahmen) ws.getCell(`${c}${ueb}`).value = { formula: `${c}${uve}-${c}${entnahmen}` }
    else if (ueb && uve) ws.getCell(`${c}${ueb}`).value = { formula: `${c}${uve}` }
    if (konto && liquidit && ueb) {
      if (m === FIRST_M) ws.getCell(`${c}${konto}`).value = 0
      else {
        const prev = monthCol(m - 1)
        ws.getCell(`${c}${konto}`).value = { formula: `${prev}${liquidit}` }
      }
      ws.getCell(`${c}${liquidit}`).value = { formula: `${c}${konto}+${c}${ueb}` }
    }
  }
}

function buildGuV(wb: ExcelJS.Workbook, data: ScenarioData, refs: SheetRefs): void {
  const ws = wb.addWorksheet(SHEET.GuV)
  writeYearHeader(ws)

  let r = 2
  for (const row of data.guv) {
    refs.guvByLabel.set((row as any).row_label as string, r)
    ws.getCell(`A${r}`).value = (row as any).row_label
    if ((row as any).is_sum_row) ws.getCell(`A${r}`).font = { bold: true }
    r++
  }

  // For each year, the columns B..F (idx 2..6) correspond to 2026..2030 (months m=1..12, 13..24, ..., 49..60).
  const yearCol = (y: number) => col(y - START_YEAR + 2)
  const monthRangeFor = (y: number, sheet: string, row: number) => {
    const startM = Math.max(FIRST_M, (y - START_YEAR) * 12 + 1)
    const endM = startM > (y - START_YEAR) * 12 + 12 ? startM : (y - START_YEAR) * 12 + 12
    return `SUM(${S(sheet)}!${monthCol(startM)}${row}:${monthCol(endM)}${row})`
  }

  // For each GuV row, build formula by year
  for (const row of data.guv) {
    const label = (row as any).row_label as string
    const rr = refs.guvByLabel.get(label)!

    const setEachYear = (fn: (y: number) => string | { formula: string } | number) => {
      for (let y = START_YEAR; y <= START_YEAR + 4; y++) {
        const c = yearCol(y)
        const v = fn(y)
        if (typeof v === 'string') ws.getCell(`${c}${rr}`).value = { formula: v }
        else if (typeof v === 'number') ws.getCell(`${c}${rr}`).value = v
        else ws.getCell(`${c}${rr}`).value = v
      }
    }

    if (label === 'Umsatzerlöse' || label === 'Gesamtleistung') {
      setEachYear(y => monthRangeFor(y, SHEET.Umsatz, refs.umsatzByLabel.get('GESAMTUMSATZ')!))
      continue
    }
    if (label === 'Summe Materialaufwand') {
      setEachYear(y => monthRangeFor(y, SHEET.Material, refs.materialByLabel.get('SUMME')!))
      continue
    }
    if (label === 'Rohergebnis') {
      const umsR = refs.guvByLabel.get('Umsatzerlöse')!
      const matR = refs.guvByLabel.get('Summe Materialaufwand')!
      setEachYear(y => `${yearCol(y)}${umsR}-${yearCol(y)}${matR}`)
      continue
    }
    if (label === 'Löhne und Gehälter') {
      setEachYear(y => monthRangeFor(y, SHEET.Personal, refs.personalSummary.brutto))
      continue
    }
    if (label === 'Soziale Abgaben') {
      setEachYear(y => monthRangeFor(y, SHEET.Personal, refs.personalSummary.sozial))
      continue
    }
    if (label === 'Summe Personalaufwand') {
      const lR = refs.guvByLabel.get('Löhne und Gehälter')!
      const sR = refs.guvByLabel.get('Soziale Abgaben')!
      setEachYear(y => `${yearCol(y)}${lR}+${yearCol(y)}${sR}`)
      continue
    }
    if (label === 'Abschreibungen') {
      setEachYear(y => monthRangeFor(y, SHEET.Invest, refs.investTotals.afa))
      continue
    }
    if (label === 'Sonst. betriebl. Aufwendungen') {
      const sonstSum = [...refs.betriebByLabel.entries()].find(([k]) => k.includes('Summe sonstige'))?.[1]
      if (sonstSum) setEachYear(y => monthRangeFor(y, SHEET.Betrieb, sonstSum))
      continue
    }
    if (label === 'Sonst. betriebl. Erträge') {
      // SonstErtraege sheet is dropped (empty in DB) — leave as 0
      continue
    }
    if (label === 'Summe sonst. Erträge') {
      const sR = refs.guvByLabel.get('Sonst. betriebl. Erträge')!
      setEachYear(y => `${yearCol(y)}${sR}`)
      continue
    }
    if (label === 'EBIT') {
      const um = refs.guvByLabel.get('Umsatzerlöse')!
      const mat = refs.guvByLabel.get('Summe Materialaufwand')!
      const per = refs.guvByLabel.get('Summe Personalaufwand')!
      const abr = refs.guvByLabel.get('Abschreibungen')!
      const sonst = refs.guvByLabel.get('Sonst. betriebl. Aufwendungen')!
      setEachYear(y => {
        const c = yearCol(y)
        return `${c}${um}-${c}${mat}-${c}${per}-${c}${abr}-${c}${sonst}`
      })
      continue
    }
    if (label === 'Ergebnis nach Steuern' || label === 'Jahresüberschuss') {
      const ebit = refs.guvByLabel.get('EBIT')!
      const steu = refs.guvByLabel.get('Steuern gesamt')!
      const zinsE = refs.guvByLabel.get('Zinserträge')!
      const zinsA = refs.guvByLabel.get('Zinsaufwendungen')!
      setEachYear(y => `${yearCol(y)}${ebit}+${yearCol(y)}${zinsE}-${yearCol(y)}${zinsA}-${yearCol(y)}${steu}`)
      continue
    }
    if (label === 'Steuern gesamt') {
      const gst = refs.guvByLabel.get('Gewerbesteuer')!
      const kst = refs.guvByLabel.get('Körperschaftssteuer')!
      setEachYear(y => `${yearCol(y)}${gst}+${yearCol(y)}${kst}`)
      continue
    }
    // Körperschaftssteuer / Gewerbesteuer: keep stored values (tax with Verlustvortrag is too complex to inline)
    // Other rows (Materialaufwand Waren/Leistungen, Bestandsveränderungen, Zinserträge, Zinsaufwendungen, Sonstige Steuern): raw values from DB
    const v = (row as any).values as Record<string, number>
    for (let y = START_YEAR; y <= START_YEAR + 4; y++) {
      ws.getCell(`${yearCol(y)}${rr}`).value = Number(v?.[`y${y}`] || 0)
    }
  }
}

/**
 * Dashboard sheet with KPI tables that chart drivers will reference.
 * Layout:
 *   Annual KPIs (B2..F11): Year | Umsatz | Material | Personnel | AfA | Sonst Opex | EBIT | Steuern | Jahresueberschuss
 *   Monthly Liquidity (B14:?): Month label header + value row
 *   Monthly Headcount (B17): label header + value row
 *   Monthly Personalkosten (B20): label header + value row
 *
 * Charts are added by the openpyxl post-processor referencing these ranges.
 */
function buildDashboard(wb: ExcelJS.Workbook, data: ScenarioData, refs: SheetRefs, scenarioName: string): void {
  const ws = wb.addWorksheet(SHEET.Dashboard)
  ws.getCell('A1').value = scenarioName
  ws.getCell('A1').font = { bold: true, size: 16 }
  ws.getColumn(1).width = 28

  // Annual KPI table (rows 3..11 to leave room): columns A=Label, B..F=2026..2030
  const yearCol = (y: number) => col(y - START_YEAR + 2)
  ws.getCell('A3').value = 'Jahres-KPI'
  ws.getCell('A3').font = { bold: true }
  for (let y = START_YEAR; y < START_YEAR + 5; y++) {
    const c = yearCol(y)
    ws.getCell(`${c}3`).value = y
    ws.getCell(`${c}3`).font = { bold: true }
    ws.getColumn(c.charCodeAt(0) - 64).width = 14
  }

  // Metrics map to GuV rows (formula-based pull). Some need rebuilt formulas.
  const metrics: { label: string; build: (c: string) => string }[] = [
    { label: 'Umsatzerlöse',         build: c => `${S(SHEET.GuV)}!${c}${refs.guvByLabel.get('Umsatzerlöse')!}` },
    { label: 'Materialaufwand',      build: c => `${S(SHEET.GuV)}!${c}${refs.guvByLabel.get('Summe Materialaufwand')!}` },
    { label: 'Personalkosten',       build: c => `${S(SHEET.GuV)}!${c}${refs.guvByLabel.get('Summe Personalaufwand')!}` },
    { label: 'Abschreibungen',       build: c => `${S(SHEET.GuV)}!${c}${refs.guvByLabel.get('Abschreibungen')!}` },
    { label: 'Sonst. betr. Aufwand', build: c => `${S(SHEET.GuV)}!${c}${refs.guvByLabel.get('Sonst. betriebl. Aufwendungen')!}` },
    { label: 'EBIT',                 build: c => `${S(SHEET.GuV)}!${c}${refs.guvByLabel.get('EBIT')!}` },
    { label: 'Steuern',              build: c => `${S(SHEET.GuV)}!${c}${refs.guvByLabel.get('Steuern gesamt')!}` },
    { label: 'Jahresüberschuss',     build: c => `${S(SHEET.GuV)}!${c}${refs.guvByLabel.get('Jahresüberschuss')!}` },
  ]
  metrics.forEach((m, i) => {
    const rr = 4 + i
    ws.getCell(`A${rr}`).value = m.label
    if (m.label === 'EBIT' || m.label === 'Jahresüberschuss') ws.getCell(`A${rr}`).font = { bold: true }
    for (let y = START_YEAR; y < START_YEAR + 5; y++) {
      const c = yearCol(y)
      ws.getCell(`${c}${rr}`).value = { formula: m.build(c) }
      ws.getCell(`${c}${rr}`).numFmt = NUMFMT
    }
  })

  // Monthly drivers — each driver gets a 2-row block: header (month label) + values
  const blockBase = 4 + metrics.length + 2 // ~14
  const writeMonthlyBlock = (label: string, baseRow: number, formulaBuilder: (c: string) => string): void => {
    ws.getCell(`A${baseRow}`).value = label
    ws.getCell(`A${baseRow}`).font = { bold: true }
    ws.getCell(`A${baseRow + 1}`).value = 'Monat'
    ws.getCell(`A${baseRow + 2}`).value = label
    for (const m of visibleMonths()) {
      const c = monthCol(m)
      const d = monthToDate(m)
      const monthName = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][d.getUTCMonth()]
      ws.getCell(`${c}${baseRow + 1}`).value = `${monthName} ${d.getUTCFullYear()}`
      ws.getCell(`${c}${baseRow + 2}`).value = { formula: formulaBuilder(c) }
      ws.getCell(`${c}${baseRow + 2}`).numFmt = NUMFMT
    }
  }

  // Liquidity
  const liqLabel = data.liquid.find(r => (r as any).row_type === 'kontostand' && ((r as any).row_label as string).includes('LIQUIDIT'))
  const liqRow = liqLabel ? refs.liquidByLabel.get((liqLabel as any).row_label as string)! : 0
  writeMonthlyBlock('Liquidität (monatlich)', blockBase, c => `${S(SHEET.Liquid)}!${c}${liqRow}`)

  // Headcount
  writeMonthlyBlock('Headcount (monatlich)', blockBase + 4, c => `${S(SHEET.Personal)}!${c}${refs.personalSummary.headcount}`)

  // Personalkosten total
  writeMonthlyBlock('Personalkosten total (monatlich)', blockBase + 8, c => `${S(SHEET.Personal)}!${c}${refs.personalSummary.total}`)

  ws.views = [{ state: 'frozen', xSplit: 1, ySplit: 3 }]
}

function buildFormulas(wb: ExcelJS.Workbook, data: ScenarioData): void {
  const ws = wb.addWorksheet(SHEET.Formulas)
  ws.getCell('A1').value = 'Sheet'
  ws.getCell('B1').value = 'Row'
  ws.getCell('C1').value = 'Formel-Beschreibung'
  ws.getRow(1).font = { bold: true }
  ws.getColumn(1).width = 18
  ws.getColumn(2).width = 50
  ws.getColumn(3).width = 80

  let r = 2
  const push = (sheet: string, label: string, desc: string) => {
    ws.getCell(`A${r}`).value = sheet
    ws.getCell(`B${r}`).value = label
    ws.getCell(`C${r}`).value = desc
    r++
  }

  push('Kunden', '... gesamt', 'SUM ueber 3 Segmente')
  push('Umsatzerloese', 'Umsatz (X)', 'Preis * Anzahl Kunden im Tier X')
  push('Umsatzerloese', 'GESAMTUMSATZ', 'SUM aller Umsatz-Zeilen + Beratung & Service')
  push('Materialaufwand', 'Cloud-Hosting', 'MAX(0, Bestandskunden_gesamt - 10) * 100 + 1500 (ab Aug 2026)')
  push('Materialaufwand', 'SUMME', 'SUM aller Cost-Zeilen')
  push('Personalkosten', 'Brutto je Person', 'IF(Monat in [start..end], Brutto * (1+raise)^Jahre_seit_start, 0)')
  push('Personalkosten', 'Sozial je Person', 'Brutto * AG-Sozial% / 100')
  push('Personalkosten', 'Total je Person', 'Brutto + Sozial')
  push('Personalkosten', 'TOTAL Personalkosten', 'SUM ueber alle Positionen')
  push('Personalkosten', 'Headcount', 'COUNTIF Brutto>0 in der Spalte')
  push('Investitionen', 'Ausgabe je Position', 'IF(Monat == Anschaffungsmonat, Betrag, 0)')
  push('Investitionen', 'AfA je Position', 'IF AfA-Jahre vorhanden: linear ueber afa_jahre*12 Monate, sonst voll im Anschaffungsmonat (GWG)')
  push('Betriebliche', 'Personalkosten (Zeile)', '=Personalkosten!TOTAL Personalkosten')
  push('Betriebliche', 'Abschreibungen (Zeile)', '=Investitionen!TOTAL AfA')
  push('Betriebliche', 'Fort-/Weiterbildungskosten', 'Headcount(ohne Gruender) * 300, ab Aug 2026')
  push('Betriebliche', 'Reisekosten', 'Headcount * 75, ab Aug 2026')
  push('Betriebliche', 'Bewirtungskosten', 'Bestandskunden_gesamt * 50, ab Aug 2026')
  push('Betriebliche', 'Internet/Mobilfunk', 'Headcount * 50, ab Aug 2026')
  push('Betriebliche', 'Berufsgenossenschaft', '0,5% von Brutto-Personalkosten')
  push('Betriebliche', 'Allgemeine Marketingkosten', '8% von Gesamtumsatz bis Dez 2028, 10% ab Jan 2029')
  push('Betriebliche', 'Gewerbesteuer (F)', '12,25% vom monatlichen Profit (falls positiv); Profit = Umsatz - Material - Personal - AfA - Rest-Opex')
  push('Betriebliche', 'Kategorie-Summen', 'SUM aller Detailzeilen der Kategorie')
  push('Betriebliche', 'Summe sonstige', 'SUM aller Betrieb-Zeilen ohne Personal/Abschr./Sum-Zeilen')
  push('Betriebliche', 'Gesamtkosten', 'Personalkosten + Abschreibungen + Summe sonstige')
  push('Liquiditaet', 'Umsatzerloese', '=Umsatzerlöse!GESAMTUMSATZ')
  push('Liquiditaet', 'Materialaufwand', '=Materialaufwand!SUMME')
  push('Liquiditaet', 'Personalkosten', '=Personalkosten!TOTAL')
  push('Liquiditaet', 'Sonstige Kosten', '=Betriebliche Aufwendungen!Summe sonstige')
  push('Liquiditaet', 'Investitionen', '=Investitionen!TOTAL Investitionsausgaben')
  push('Liquiditaet', 'Summe ERTRAEGE', 'SUM aller Einzahlungen')
  push('Liquiditaet', 'Summe AUSZAHLUNGEN', 'SUM aller Auszahlungen')
  push('Liquiditaet', 'UEBERSCHUSS VOR INVEST.', 'Summe ERTRAEGE - Summe AUSZAHLUNGEN')
  push('Liquiditaet', 'UEBERSCHUSS VOR ENTN.', 'UEBERSCHUSS VOR INVEST - Investitionen')
  push('Liquiditaet', 'UEBERSCHUSS', 'UEBERSCHUSS VOR ENTN - Kapitalentnahmen')
  push('Liquiditaet', 'Kontostand (Monatsbeginn)', '0 in m1, sonst LIQUIDITAET des Vormonats')
  push('Liquiditaet', 'LIQUIDITAET', 'Kontostand + UEBERSCHUSS')
  push('Liquiditaet', 'Gewerbe-/Koerperschaftsteuer', 'Aus DB uebernommen (Verlustvortrag-Logik nicht in Excel inline)')
  push('GuV', 'Umsatzerloese / Gesamtleistung', 'SUM Umsatzerloese!GESAMTUMSATZ ueber das Jahr')
  push('GuV', 'Summe Materialaufwand', 'SUM Materialaufwand!SUMME ueber das Jahr')
  push('GuV', 'Rohergebnis', 'Umsatzerloese - Summe Materialaufwand')
  push('GuV', 'Loehne und Gehaelter', 'SUM Personalkosten!TOTAL Brutto')
  push('GuV', 'Soziale Abgaben', 'SUM Personalkosten!TOTAL Sozial')
  push('GuV', 'Summe Personalaufwand', 'Loehne + Soziale Abgaben')
  push('GuV', 'Abschreibungen', 'SUM Investitionen!TOTAL AfA')
  push('GuV', 'Sonst. betr. Aufwendungen', 'SUM Betriebliche Aufwendungen!Summe sonstige')
  push('GuV', 'EBIT', 'Umsatz - Material - Personal - AfA - Sonst. betr. Aufwand')
  push('GuV', 'Koerperschaft-/Gewerbesteuer', 'Aus DB uebernommen (Verlustvortrag-Logik)')
  push('GuV', 'Ergebnis nach Steuern / Jahresueberschuss', 'EBIT + Zinsertraege - Zinsaufwendungen - Steuern gesamt')
}

// =====================================================================
// MAIN
// =====================================================================
async function main() {
  const pool = new Pool({ connectionString: CONN, ssl: false })
  const outDir = path.join(__dirname, '..', 'exports')
  require('fs').mkdirSync(outDir, { recursive: true })

  for (const sc of SCENARIOS) {
    console.log(`\n=== Exporting ${sc.slug} (${sc.id}) ===`)
    const data = await loadScenario(pool, sc.id)
    if (!data.scenario) {
      console.log('  Scenario not found, skip')
      continue
    }
    const refs: SheetRefs = {
      kunden: new Map(),
      umsatzByLabel: new Map(),
      materialByLabel: new Map(),
      personalInputRow: new Map(),
      personalBruttoRow: new Map(),
      personalSozialRow: new Map(),
      personalTotalRow: new Map(),
      personalSummary: { brutto: 0, sozial: 0, total: 0, headcount: 0, founderHc: 0 },
      investInputRow: new Map(),
      investInvestRow: new Map(),
      investAfaRow: new Map(),
      investTotals: { invest: 0, afa: 0 },
      betriebByLabel: new Map(),
      sonstByIdx: new Map(),
      sonstSumGesamt: 0,
      liquidByLabel: new Map(),
      guvByLabel: new Map(),
    }

    const wb = new ExcelJS.Workbook()
    wb.creator = 'BreakPilot Finanzplan Export'
    wb.created = new Date()

    // Order matters: dependent sheets reference earlier ones.
    // SonstErtraege is skipped — DB has 0 values for all rows across all scenarios.
    buildKunden(wb, data, refs)
    buildUmsatz(wb, data, refs)
    buildPersonal(wb, data, refs)
    buildInvest(wb, data, refs)
    buildMaterial(wb, data, refs)
    buildBetrieb(wb, data, refs)
    buildLiquid(wb, data, refs)
    buildGuV(wb, data, refs)
    // Dashboard must be built after GuV (uses guvByLabel) and Liquid/Personal/Material refs.
    buildDashboard(wb, data, refs, `${(data.scenario as any).name} — Finanzplan`)
    buildFormulas(wb, data)

    // Sheet order is handled by the Python post-processor.

    // Apply zero-suppressing number format to every data sheet (skip the docs tab, which is text)
    wb.eachSheet(s => {
      if (s.name === SHEET.Formulas) return
      applyNumFmtToSheet(s)
    })

    const file = path.join(outDir, `Finanzplan-${sc.slug}.xlsx`)
    await wb.xlsx.writeFile(file)
    console.log(`  Wrote ${file}`)
  }

  await pool.end()
}

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