// ===== Absatzprognose — Algorithmus (analog ML-Notebook, deterministisch in JS) =====
// Pro Artikel werden aus der Abruf-Historie Wiederbestellintervalle (reorder_time) und
// Bestellmengen (salesqty) analysiert. Vorhergesagt werden — rollierend in die Zukunft —
// ZEITPUNKT (predicted_usage) und MENGE (predicted_salesqty) je Abruf.

const DAY_MS = 86400000;
const dParse = (s) => new Date(s + 'T00:00:00Z');
const mean = (a) => a.length ? a.reduce((x, y) => x + y, 0) / a.length : 0;
function std(a) { if (a.length < 2) return 0; const m = mean(a); return Math.sqrt(a.reduce((s, x) => s + (x - m) ** 2, 0) / (a.length - 1)); }
// lineare Regression: gibt {slope, intercept} (x = Index 0..n-1)
function linreg(ys) {
  const n = ys.length; if (n < 2) return { slope: 0, intercept: ys[0] || 0 };
  const xm = (n - 1) / 2, ym = mean(ys);
  let num = 0, den = 0;
  for (let i = 0; i < n; i++) { num += (i - xm) * (ys[i] - ym); den += (i - xm) ** 2; }
  const slope = den ? num / den : 0;
  return { slope, intercept: ym - slope * xm };
}

// Monatliche Saisonfaktoren aus der Historie (multiplikativ, normalisiert auf Mittel 1)
function seasonalFactors(rows) {
  const sum = Array(12).fill(0), cnt = Array(12).fill(0);
  rows.forEach(r => { const m = dParse(r.datum).getUTCMonth(); sum[m] += r.menge; cnt[m]++; });
  const avg = rows.length ? mean(rows.map(r => r.menge)) : 1;
  const f = sum.map((s, i) => (cnt[i] > 0 ? (s / cnt[i]) / (avg || 1) : 1));
  // glätten gegen Übersteuerung
  return f.map(x => 0.5 + 0.5 * x);
}

// Kennzahlen je Artikel berechnen
function analyzeArticle(rows) {
  const sorted = rows.slice().sort((a, b) => a.datum.localeCompare(b.datum));
  const intervals = [];
  for (let i = 1; i < sorted.length; i++) intervals.push((dParse(sorted[i].datum) - dParse(sorted[i - 1].datum)) / DAY_MS);
  const qtys = sorted.map(r => r.menge);
  const last5iv = intervals.slice(-5), last5qty = qtys.slice(-5);
  const ivMeanAll = mean(intervals), ivMean5 = mean(last5iv) || ivMeanAll;
  const ivStd = std(intervals);
  const qtyTrend = linreg(qtys); // slope je Abruf-Index
  const season = seasonalFactors(sorted);
  return {
    n: sorted.length, first: sorted[0] && sorted[0].datum, last: sorted[sorted.length - 1] && sorted[sorted.length - 1].datum,
    intervals, qtys,
    ivMeanAll, ivMean5, ivStd, ivCv: ivMeanAll ? ivStd / ivMeanAll : 0,
    qtyMeanAll: mean(qtys), qtyMean5: mean(last5qty) || mean(qtys), qtyStd: std(qtys), qtyCv: mean(qtys) ? std(qtys) / mean(qtys) : 0,
    qtySlope: qtyTrend.slope, qtyIntercept: qtyTrend.intercept, season,
  };
}

// Rollierende Prognose: ab letztem Abruf werden zukünftige Abrufe vorhergesagt
// (Zeitpunkt = gewichteter Intervall-Mittelwert, Menge = Trend × Saison).
function forecastArticle(rows, opts = {}) {
  const horizonDays = opts.horizonDays || 270;
  const st = analyzeArticle(rows);
  if (st.n < 2) return { stats: st, points: [] };
  const today = window.FC_TODAY;
  // gewichtetes Intervall: 60% jüngste 5, 40% gesamt (analog mean_last_5 / mean_all-Features)
  const ivPred = 0.6 * st.ivMean5 + 0.4 * st.ivMeanAll;
  const sorted = rows.slice().sort((a, b) => a.datum.localeCompare(b.datum));
  let cur = dParse(sorted[sorted.length - 1].datum);
  let idx = st.n - 1;
  const end = window.fcAddDays(today, horizonDays);
  const points = [];
  let guard = 0;
  while (guard++ < 60) {
    cur = window.fcAddDays(cur, Math.max(4, Math.round(ivPred)));
    idx++;
    if (cur > end) break;
    // Mengen-Trend (gedämpft) + Saison
    const base = 0.55 * st.qtyMean5 + 0.45 * (st.qtyIntercept + st.qtySlope * idx);
    const seas = st.season[cur.getUTCMonth()] || 1;
    let q = base * seas;
    q = Math.min(10000, Math.max(300, Math.round(q / 50) * 50));
    // Prognoseband aus Streuung
    const bandRel = Math.min(0.6, 0.35 + st.qtyCv * 0.5);
    points.push({
      datum: window.fcIso(cur), menge: q,
      lo: Math.max(0, Math.round(q * (1 - bandRel) / 50) * 50),
      hi: Math.round(q * (1 + bandRel) / 50) * 50,
      future: cur >= today,
    });
  }
  return { stats: st, ivPred, points };
}

// Kunden-Abrufplan gegen die ML-Prognose stellen → Abweichungs-Score & Flags
function comparePlan(forecast, abrufplan) {
  const pts = forecast.points;
  if (!pts.length) return { matches: [], totalCust: 0, totalFc: 0, devPct: 0, flags: [] };
  const flags = [];
  const matches = abrufplan.map(c => {
    const cd = dParse(c.datum);
    // nächstgelegener Prognosepunkt (±14 Tage)
    let best = null, bestDelta = Infinity;
    pts.forEach(p => { const dd = Math.abs((dParse(p.datum) - cd) / DAY_MS); if (dd < bestDelta) { bestDelta = dd; best = p; } });
    const within = best && bestDelta <= 18;
    const qtyDev = best ? (c.menge - best.menge) / (best.menge || 1) : 0;
    const outOfBand = best && (c.menge < best.lo || c.menge > best.hi);
    if (outOfBand) flags.push({ datum: c.datum, type: 'menge', msg: `Menge ${c.menge.toLocaleString('de-DE')} außerhalb Prognoseband (${best.lo.toLocaleString('de-DE')}–${best.hi.toLocaleString('de-DE')})` });
    if (!within && best) flags.push({ datum: c.datum, type: 'zeit', msg: `Termin ${bestDelta.toFixed(0)} Tage von erwartetem Abruf entfernt` });
    return { cust: c, fc: best, dateDeltaDays: best ? Math.round((cd - dParse(best.datum)) / DAY_MS) : null, qtyDev, outOfBand, within };
  });
  const totalCust = abrufplan.reduce((s, c) => s + c.menge, 0);
  const futureFc = pts.filter(p => p.future);
  const totalFc = futureFc.reduce((s, p) => s + p.menge, 0);
  const devPct = totalFc ? (totalCust - totalFc) / totalFc * 100 : 0;
  return { matches, totalCust, totalFc, devPct, flags };
}

Object.assign(window, { analyzeArticle, forecastArticle, comparePlan, fcMean: mean, fcStd: std });
