// valuations.jsx — pure valuation math. No UI here.
// All money values are per-share in the stock's own currency.
// Rates (r, g, terminalGrowth, safetyMargin) are decimals (0.07 = 7%).

// ── helpers ──────────────────────────────────────────────────────────────
function median(nums) {
  const a = nums.filter((n) => Number.isFinite(n)).sort((x, y) => x - y);
  if (!a.length) return NaN;
  const m = Math.floor(a.length / 2);
  return a.length % 2 ? a[m] : (a[m - 1] + a[m]) / 2;
}
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
// Minimale spread tussen disconteringsvoet en eeuwige groei. Daaronder explodeert
// elke terminalwaarde (1/(r−g) → ∞) en zegt de uitkomst niets meer.
const MIN_SPREAD = 0.02;

// ── DIVIDEND METHODS ───────────────────────────────────────────────────────
// Simple Yield: prijs waarbij het huidige dividend exact het gewenste
// dividendrendement y oplevert.  P = D / y
function simpleYieldPrice(D, r) {
  if (!(D > 0) || !(r > 0)) return NaN;
  return D / r;
}
// Gordon Growth (Dividend Discount Model): waardeert een eeuwig groeiend
// dividend.  P = D(1+g) / (r − g),  vereist r > g.
function gordonGrowthPrice(D, g, r) {
  if (!(D > 0) || !(r > 0) || !(r > g)) return NaN;
  return (D * (1 + g)) / (r - g);
}
// Twee-fase Dividend Discount Model: `years` jaar dividend groeiend met (gecapte)
// g, daarna een Gordon-terminalwaarde op terminalGrowth — alles verdisconteerd
// tegen de KOSTENVOET EIGEN VERMOGEN r (totaal vereist rendement), niet tegen het
// gewenste dividendrendement. Vereist r − terminalGrowth ≥ MIN_SPREAD.
function twoStageDDM({ dividend, g, r, terminalGrowth = 0.025, years = 5, gCap = 0.15 }) {
  if (!(dividend > 0) || !(r - terminalGrowth >= MIN_SPREAD - 1e-9)) return NaN;
  const gEff = clamp(Number.isFinite(g) ? g : 0, -0.5, gCap);
  let pv = 0, d = dividend;
  for (let t = 1; t <= years; t++) {
    d *= (1 + gEff);
    pv += d / Math.pow(1 + r, t);
  }
  const terminal = (d * (1 + terminalGrowth)) / (r - terminalGrowth);
  return pv + terminal / Math.pow(1 + r, years);
}
function dividendValuation(stock, inp) {
  const D = inp.dividend, y = inp.requiredYield, g = inp.divGrowth;
  const r = inp.discountRate, tg = inp.terminalGrowth;
  const simple = simpleYieldPrice(D, y);
  const ddm = twoStageDDM({ dividend: D, g, r, terminalGrowth: tg });
  const composite = median([simple, ddm]);
  return {
    methods: [
      { key: 'simple', label: 'Simpel rendement', value: simple,
        formula: 'P = D / y',
        plug: `${fmtMoney(D, stock.currency)} / ${pct(y)}`,
        note: 'Welke koers levert precies jouw gewenste dividendrendement op, los van groei.' },
      { key: 'ddm', label: 'Dividend Discount (2-fase)', value: ddm,
        formula: 'Σ D(1+g)ᵗ / (1+r)ᵗ  +  TV / (1+r)⁵',
        plug: `${fmtMoney(D, stock.currency)} · g ${pct(clamp(Number.isFinite(g) ? g : 0, -0.5, 0.15))} (5j) · r ${pct(r)} · TV-groei ${pct(tg)}`,
        note: '5 jaar dividend groeiend met g, daarna eeuwig met de terminale groei; contant gemaakt tegen de kostenvoet eigen vermogen. Vereist r − terminale groei ≥ 2%.' },
    ],
    composite,
    compositeLabel: 'Mediaan (= gemiddelde) van beide dividendmethodes',
  };
}

// ── GROWTH METHODS ───────────────────────────────────────────────────────
// Two-stage DCF: 10 jaar expliciete vrije kasstroom per aandeel — jaar 1–5
// groeiend met (gecapte) g, jaar 6–10 lineair afbouwend naar terminalGrowth —
// plus Gordon-terminalwaarde, alles contant gemaakt tegen r. Tien jaar vlak op
// een hoge g is een bekende overwaarderingsbias; de afbouw dempt die.
function dcfGrowthPath(g, terminalGrowth, years = 10, highYears = 5) {
  const path = [];
  for (let t = 1; t <= years; t++) {
    if (t <= highYears) path.push(g);
    else path.push(g + ((t - highYears) / (years - highYears)) * (terminalGrowth - g));
  }
  return path;
}
function twoStageDCF({ cashflow, g, gCap = 0.15, r, terminalGrowth = 0.025, years = 10 }) {
  if (!(cashflow > 0) || !(r - terminalGrowth >= MIN_SPREAD - 1e-9)) return NaN;
  const gEff = clamp(g, -0.5, gCap);
  let pv = 0, cf = cashflow;
  dcfGrowthPath(gEff, terminalGrowth, years).forEach((gt, i) => {
    cf *= (1 + gt);
    pv += cf / Math.pow(1 + r, i + 1);
  });
  const terminal = (cf * (1 + terminalGrowth)) / (r - terminalGrowth);
  pv += terminal / Math.pow(1 + r, years);
  return pv;
}
// Peter Lynch — eerlijke koers waar PEG = 1, dus eerlijke K/W = groei in %.
// FV = WPA × groei%.  (gecapt om absurde multiples te vermijden)
function peterLynchValue(eps, g) {
  if (!(eps > 0)) return NaN;
  const fairPE = clamp(g * 100, 5, 40);
  return eps * fairPE;
}
// Graham Growth (herziene formule): V = WPA × (8.5 + 2g) × 4.4 / Y
// g = groei in %, Y = rendement AAA-bedrijfsobligaties in %.
function grahamGrowthValue(eps, g, bondYield = 4.8) {
  if (!(eps > 0)) return NaN;
  const gPct = clamp(g * 100, 0, 20);
  return (eps * (8.5 + 2 * gPct) * 4.4) / bondYield;
}
function growthValuation(stock, inp) {
  const dcf = twoStageDCF({
    cashflow: inp.cashflowPerShare, g: inp.growth, gCap: inp.growthCap,
    r: inp.discountRate, terminalGrowth: inp.terminalGrowth,
  });
  const lynch = peterLynchValue(inp.eps, inp.growth);
  const graham = grahamGrowthValue(inp.eps, inp.growth, inp.bondYield);
  const composite = median([dcf, lynch, graham]);
  return {
    methods: [
      { key: 'dcf', label: 'Two-stage DCF', value: dcf,
        formula: 'Σ FCF·Π(1+gₜ) / (1+r)ᵗ  +  TV / (1+r)¹⁰',
        plug: `FCF ${fmtMoney(inp.cashflowPerShare, stock.currency)} · g ${pct(Math.min(inp.growth, inp.growthCap))} (cap ${pct(inp.growthCap)}) j1–5, afbouw naar ${pct(inp.terminalGrowth)} j6–10 · r ${pct(inp.discountRate)}`,
        note: '10 jaar vrije kasstroom (5 jaar op g, daarna afbouw naar de terminale groei) + Gordon-terminalwaarde, contant gemaakt. Vereist r − terminale groei ≥ 2%.' },
      { key: 'lynch', label: 'Peter Lynch (PEG = 1)', value: lynch,
        formula: 'FV = WPA × groei%',
        plug: `${fmtMoney(inp.eps, stock.currency)} × ${(clamp(inp.growth * 100, 5, 40)).toFixed(0)}`,
        note: 'Eerlijke koers wanneer de K/W gelijk is aan het groeipercentage.' },
      { key: 'graham', label: 'Graham Growth', value: graham,
        formula: 'V = WPA × (8.5 + 2g) × 4.4 / Y',
        plug: `${fmtMoney(inp.eps, stock.currency)} × (8.5 + 2·${(clamp(inp.growth * 100, 0, 20)).toFixed(0)}) × 4.4 / ${inp.bondYield}`,
        note: 'Herziene Graham-formule, genormaliseerd op de AAA-obligatierente.' },
    ],
    composite,
    // NB: Lynch (K/W = g) ligt altijd onder Graham (K/W ≈ 7,8 + 1,8·g), dus de
    // mediaan is feitelijk de DCF begrensd tussen Lynch en Graham.
    compositeLabel: 'Mediaan van DCF, Lynch en Graham',
  };
}

// ── BUY PRICE + STATUS ─────────────────────────────────────────────────────
function buyPrice(composite, safetyMargin) {
  return composite * (1 - safetyMargin);
}
// status t.o.v. reële (composite) waarde, met 5% neutrale band
function valuationStatus(price, composite) {
  if (!(price > 0) || !Number.isFinite(composite) || composite <= 0) return 'unknown';
  const ratio = price / composite;
  if (ratio < 0.95) return 'under';
  if (ratio > 1.05) return 'over';
  return 'fair';
}
const STATUS_NL = {
  under: 'Ondergewaardeerd', fair: 'Redelijk gewaardeerd',
  over: 'Overgewaardeerd', unknown: 'Onbekend',
};

// ── CAGR ─────────────────────────────────────────────────────────────────
// Historisch totaalrendement (incl. dividend), geannualiseerd. De dividend-
// bijdrage komt uit de ECHTE jaarreeks binnen het venster (gemiddeld uitgekeerd
// dividend ÷ gemiddelde koers); alleen zonder historie valt hij terug op het
// huidige dividend als constante yield (ruwer, overschat bij groeiers).
// De startkoers is de jaarslotkoers van het startjaar: dividenden uit dat jaar
// vallen vóór het venster en tellen niet mee.
// Minimaal venster: korter dan dit is geen "jaarrendement" maar ruis (een net
// genoteerd aandeel kan anders +200%/jaar tonen over 9 maanden).
const MIN_CAGR_YEARS = 3;
function historicalCAGR(history, annualDividend, divHistory, now = Date.now()) {
  if (!history || history.length < 2) return NaN;
  const start = history[0], end = history[history.length - 1];
  // startkoers = jaarslot van het startjaar; is het eindpunt het lopende jaar,
  // dan is dat de actuele koers → tel alleen het verstreken deel van dat jaar
  const cy = new Date(now).getUTCFullYear();
  const years = end.year >= cy
    ? (now - Date.UTC(start.year, 11, 31)) / (365.25 * 86400000)
    : end.year - start.year;
  if (!(years >= MIN_CAGR_YEARS - 1e-9) || !(start.price > 0)) return NaN;
  const avgPrice = history.reduce((s, h) => s + h.price, 0) / history.length;
  let divReturn = 0;
  const win = (divHistory || []).filter((h) => h.year > start.year && h.year <= end.year && h.dividend > 0);
  if (win.length && avgPrice > 0) {
    const avgDiv = win.reduce((s, h) => s + h.dividend, 0) / years;
    divReturn = avgDiv / avgPrice;
  } else if (annualDividend > 0 && avgPrice > 0) {
    divReturn = annualDividend / avgPrice;
  }
  const priceCAGR = Math.pow(end.price / start.price, 1 / years) - 1;
  return priceCAGR + divReturn;
}
// Verwacht jaarrendement (5j): de koers convergeert in `years` jaar naar de reële
// waarde, die zelf meegroeit met g; daarbovenop het dividendrendement.
//   (1+g) · (reële waarde / koers)^(1/years) − 1 + dividend / koers
// Bij een eerlijk geprijsd aandeel is dat precies groei + yield.
function projectedCAGR({ price, composite, growth, dividend, years = 5 }) {
  if (!(price > 0) || !(composite > 0)) return NaN;
  const g = Number.isFinite(growth) ? growth : 0;
  const yld = dividend > 0 ? dividend / price : 0;
  return (1 + g) * Math.pow(composite / price, 1 / years) - 1 + yld;
}

// ── range positie ──────────────────────────────────────────────────────────
function rangePosition(price, low, high) {
  if (!(high > low)) return 0.5;
  return clamp((price - low) / (high - low), 0, 1);
}

// ── formatting (NL) ─────────────────────────────────────────────────────────
const CUR = { EUR: '€', USD: '$', GBP: '£' };
function fmtMoney(v, cur = 'EUR', dec = 2) {
  if (!Number.isFinite(v)) return '—';
  const sym = CUR[cur] || '';
  const n = v.toLocaleString('nl-NL', { minimumFractionDigits: dec, maximumFractionDigits: dec });
  return `${sym}\u00A0${n}`;
}
function fmtBig(v, cur = 'EUR') {
  if (!Number.isFinite(v)) return '—';
  const sym = CUR[cur] || '';
  const abs = Math.abs(v);
  if (abs >= 1e12) return `${sym}\u00A0${(v / 1e12).toLocaleString('nl-NL', { maximumFractionDigits: 2 })}\u00A0bln`;
  if (abs >= 1e9) return `${sym}\u00A0${(v / 1e9).toLocaleString('nl-NL', { maximumFractionDigits: 2 })}\u00A0mld`;
  if (abs >= 1e6) return `${sym}\u00A0${(v / 1e6).toLocaleString('nl-NL', { maximumFractionDigits: 2 })}\u00A0mln`;
  return fmtMoney(v, cur, 0);
}
function pct(v, dec = 1) {
  if (!Number.isFinite(v)) return '—';
  return `${(v * 100).toLocaleString('nl-NL', { minimumFractionDigits: dec, maximumFractionDigits: dec })}%`;
}
function signedPct(v, dec = 1) {
  if (!Number.isFinite(v)) return '—';
  const s = v > 0 ? '+' : '';
  return s + pct(v, dec);
}
function fmtNum(v, dec = 2) {
  if (!Number.isFinite(v)) return '—';
  return v.toLocaleString('nl-NL', { minimumFractionDigits: dec, maximumFractionDigits: dec });
}

Object.assign(window, {
  median, MIN_SPREAD, simpleYieldPrice, gordonGrowthPrice, twoStageDDM, dividendValuation,
  dcfGrowthPath, twoStageDCF, peterLynchValue, grahamGrowthValue, growthValuation,
  buyPrice, valuationStatus, STATUS_NL, MIN_CAGR_YEARS, historicalCAGR, projectedCAGR,
  rangePosition, fmtMoney, fmtBig, pct, signedPct, fmtNum,
});
