// ===== Score-Berechnung & Solver-Heuristik =====

// Kunden-Segment -> Score (im Admin parametrierbar)
const DEFAULT_KUNDE_MAP = { A: 9, B: 5, C: 2 };

// Default-Gewichtungen je Kriterium (1..9 Priorität, 0 = ignorieren)
const DEFAULT_WEIGHTS = {
  schlupf:  8,
  wert:     5,
  kunde:    6,
  planEnde: 7,
  planStart: 3,
};

const CRITERIA = [
  { key: 'schlupf',   label: 'Schlupf',            hint: 'geplanter Endtermin − Restarbeitsinhalt' },
  { key: 'wert',      label: 'Monetärer Wert',     hint: 'Auftragswert in €' },
  { key: 'kunde',     label: 'Kunde (ABC)',        hint: 'Kundensegment A/B/C' },
  { key: 'planEnde',  label: 'Plan-Ende',          hint: 'früher Endtermin → höher' },
  { key: 'planStart', label: 'Plan-Start',         hint: 'früher Starttermin → höher' },
];

const clamp9 = (v) => Math.max(1, Math.min(9, Math.round(v)));

// Schlupf in Stunden: Zeitfenster bis Endtermin minus gesamte verbleibende Restarbeit
// (alle noch offenen Vorgänge im Arbeitsplan, nicht nur die aktuelle Station)
function slackHours(o) { return (o.planEnde) - (o.restGesamtH ?? o.restH); }

// Pool-Grenzen (min/max) über alle Aufträge — für relative 1..9-Skalierung
function poolBounds() {
  if (window.__poolBounds) return window.__poolBounds;
  const pool = window.ORDERS || [];
  const mm = (sel) => { const vs = pool.map(sel); return { min: Math.min(...vs), max: Math.max(...vs) }; };
  const b = {
    slack: mm(o => slackHours(o)),
    wert: mm(o => o.wert),
    planEnde: mm(o => o.planEnde),
    planStart: mm(o => o.planStart),
  };
  window.__poolBounds = b;
  return b;
}
// kleiner Wert → hoher Score (dringlicher / früher)
const scaleInv = (v, b) => (b.max === b.min) ? 5 : clamp9(9 - ((v - b.min) / (b.max - b.min)) * 8);
// großer Wert → hoher Score
const scaleDir = (v, b) => (b.max === b.min) ? 5 : clamp9(1 + ((v - b.min) / (b.max - b.min)) * 8);

// Rohscore je Kriterium (1..9), relativ zur Spanne aller Aufträge
function rawScores(o, kundeMap) {
  const b = poolBounds();
  const seg = window.CUSTOMERS[o.kunde] || 'C';
  return {
    schlupf:   scaleInv(slackHours(o), b.slack),     // wenig Schlupf → dringend → hoch
    wert:      scaleDir(o.wert, b.wert),             // hoher Auftragswert → hoch
    kunde:     kundeMap[seg] ?? 2,                   // A/B/C-Mapping
    planEnde:  scaleInv(o.planEnde, b.planEnde),     // früher Endtermin → hoch
    planStart: scaleInv(o.planStart, b.planStart),   // früher Starttermin → hoch
  };
}

// Gesamt-Score (1..9): gewichteter Mittelwert der Rohscores
function computeScore(o, weights, kundeMap) {
  const raw = rawScores(o, kundeMap);
  let num = 0, den = 0;
  for (const c of CRITERIA) {
    const wgt = weights[c.key] || 0;
    num += wgt * raw[c.key];
    den += wgt;
  }
  const total = den > 0 ? num / den : 5;
  return { total: clamp9(total), raw };
}

// ===== Solver =====
// Greedy-Konstruktion: respektiert Frozen Zone (feste Köpfe), Restriktionen
// (Werkstoff X nicht direkt nach Y) und das Optimierungsziel (Regler).
//
// goal: { durchlauf, ruesten, termin } – Werte 0..100
function runSolver({ frozen, candidates, goal, restrictions, weights, kundeMap }) {
  // normiere Zielgewichte
  const g = { d: goal.durchlauf, r: goal.ruesten, t: goal.termin };
  const gsum = (g.d + g.r + g.t) || 1;
  const wD = g.d / gsum, wR = g.r / gsum, wT = g.t / gsum;

  const maxSetup = 55;
  const maxDur = Math.max(...candidates.map(o => o.dauerH), 1);
  const maxEnde = Math.max(...candidates.map(o => o.planEnde), 1);

  // letzter Werkstoff aus der Frozen Zone
  let prevMat = frozen.length ? frozen[frozen.length - 1].mat : null;
  const remaining = candidates.slice();
  const result = [];
  const trace = [];

  const forbids = (prevId, candId) =>
    restrictions.some(r => r.active && r.from === prevId && r.to === candId);

  while (remaining.length) {
    let best = null, bestVal = -Infinity, bestParts = null, bestForbidden = false;
    for (const cand of remaining) {
      const score = computeScore(cand, weights, kundeMap).total;
      const setup = window.setupMinutes(prevMat, cand.mat);
      const forbidden = forbids(prevMat, cand.mat);

      // Teilnutzen 0..1
      const pScore = score / 9;                       // Priorität (immer relevant)
      const pRuest = 1 - setup / maxSetup;            // wenig Rüsten = gut
      const pDurch = 1 - cand.dauerH / maxDur;        // kurze Vorgänge zuerst (SPT)
      const pTermin = 1 - cand.planEnde / maxEnde;    // früher Endtermin = gut

      // Basis: Priorität (Score) zählt immer; Regler formen die Reihenfolge
      let val = 0.9 * pScore + wR * pRuest + wD * pDurch + wT * pTermin;
      if (forbidden) val -= 1.0; // harte Restriktion stark abwerten

      if (val > bestVal) {
        bestVal = val; best = cand; bestForbidden = forbidden;
        bestParts = { score, setup, pScore, pRuest, pDurch, pTermin };
      }
    }
    result.push({ ...best, _setup: window.setupMinutes(prevMat, best.mat), _forbidden: bestForbidden });
    trace.push({ nr: best.nr, val: bestVal, ...bestParts });
    prevMat = best.mat;
    remaining.splice(remaining.indexOf(best), 1);
  }
  return { sequence: result, trace };
}

// ===== Optimierer: lokale Suche (Or-opt) auf der Reihenfolge =====
// Bewertet eine Sequenz über die echte Belegung (Rüsten, Makespan, Verspätung)
// und verbessert sie iterativ, ohne fixierte Vorgänge zu verschieben.
function evalSeq(ops, goal) {
  const sched = window.computeSchedule(ops);
  const setup = sched.blocks.reduce((a, b) => a + b.setupMin, 0);
  const makespan = sched.total;
  const DAY = window.SHIFT_HOURS * 3;
  let tard = 0;
  sched.blocks.forEach((b, i) => { const due = (Math.floor(ops[i].planEnde / DAY) + 1) * DAY; tard += Math.max(0, b.end - due); });
  return { setup, makespan, tard };
}
function optimizeSequence(ops, goal) {
  const g = { d: goal.durchlauf, r: goal.ruesten, t: goal.termin }; const s = (g.d + g.r + g.t) || 1;
  const wD = g.d / s, wR = g.r / s, wT = g.t / s;
  const base = evalSeq(ops, goal);
  const norm = { setup: base.setup || 1, makespan: base.makespan || 1, tard: base.tard || 1 };
  const J = (m) => wR * (m.setup / norm.setup) + wD * (m.makespan / norm.makespan) + wT * (m.tard / norm.tard);
  const movableSlots = ops.map((o, i) => i).filter(i => !ops[i].__locked);
  const build = (mv) => { const arr = ops.slice(); movableSlots.forEach((slot, k) => { arr[slot] = mv[k]; }); return arr; };
  let order = movableSlots.map(i => ops[i]);
  let bestJ = J(evalSeq(build(order), goal));
  let improved = true, guard = 0;
  while (improved && guard++ < 30) {
    improved = false;
    for (let a = 0; a < order.length; a++) {
      for (let b = 0; b < order.length; b++) {
        if (a === b) continue;
        const cand = order.slice(); const [it] = cand.splice(a, 1); cand.splice(b, 0, it);
        const j = J(evalSeq(build(cand), goal));
        if (j < bestJ - 1e-9) { bestJ = j; order = cand; improved = true; }
      }
    }
  }
  const bestSeq = build(order);
  const after = evalSeq(bestSeq, goal);
  const pct = (x, y) => (y ? (x - y) / y * 100 : 0);
  return { order: bestSeq.map(o => o.nr), before: base, after,
    delta: { setup: pct(after.setup, base.setup), makespan: pct(after.makespan, base.makespan), tard: pct(after.tard, base.tard) } };
}

Object.assign(window, {
  DEFAULT_KUNDE_MAP, DEFAULT_WEIGHTS, CRITERIA, computeScore, rawScores, slackHours, runSolver,
  evalSeq, optimizeSequence,
});
