/* ============================================================
   SIMULATION ENGINE — animates every process variable.
   Mutates the live fields on MODEL.DEVICES in place each tick.
   ============================================================ */
(function () {
  const D = window.MODEL.DEVICES;

  // ---- quality of an instrument vs its limits -------------------
  function qualOf(d) {
    if (d.kind !== "instrument") return "ok";
    const v = d.v, L = d.lim || {};
    if (L.HH != null && v >= L.HH) return "alarm";
    if (L.LL != null && v <= L.LL) return "alarm";
    if (L.H  != null && v >= L.H)  return "warn";
    if (L.L  != null && v <= L.L)  return "warn";
    return "ok";
  }

  // smooth bounded random walk back toward base
  function drift(d) {
    const span = (d.range[1] - d.range[0]);
    const noise = (Math.random() - 0.5) * d.vol * span * 0.02;
    const pull = (d.base - d.v) * 0.05;
    d.v = d.v + pull + noise;
    if (d.v < d.range[0]) d.v = d.range[0];
    if (d.v > d.range[1]) d.v = d.range[1];
  }

  let tk = 0;
  function tick() {
    tk++;
    for (const d of D) {
      if (d.kind === "instrument") {
        drift(d);
      } else if (d.kind === "motor") {
        if (d.fault) { d.running = false; }
        if (d.running) {
          if (d.isGenset) {
            // genset: derive from its own power tag (GG265 → 50JT01) or fixed powerKw
            const pw = d.powerKw != null ? d.powerKw : ((window.devById("50JT01") || {}).v || 0);
            d.loadPct = Math.round((pw / d.kW) * 100);
          } else {
            const wob = (Math.random() - 0.5) * 0.06;
            d.A = Math.max(0, d.Anom ? d.Anom * (1 + wob) : (d.A || d.kW * 1.8) * (1 + wob));
            if (!d.Anom) d.Anom = d.A;
            if (d.hasVfd) {
              const base = d.hzBase || d.hz || 50;
              d.hzBase = base;
              d.hz = base + (Math.random() - 0.5) * 0.8;
            }
          }
          d.runH += 1 / 3600 * 60; // demo accelerated
          d.tWind = 58 + (d.A / (d.kW * 2)) * 30 + (Math.random() - 0.5) * 1.5;
          d.tBear = 42 + (d.A / (d.kW * 2)) * 16 + (Math.random() - 0.5) * 1.2;
          d.vib = 1.8 + Math.random() * 0.9;
        } else {
          d.A = 0; d.tWind = 24 + (Math.random() - 0.5); d.tBear = 23 + (Math.random() - 0.5); d.vib = 0.2 + Math.random() * 0.2;
          d.loadPct = 0;
        }
      } else if (d.kind === "valve") {
        const target = d.open ? 100 : 0;
        if (d.pos !== target && !d.fault) {
          d.travelling = true;
          const step = 100 / (d.travel * (1000 / TICK_MS));
          if (d.pos < target) d.pos = Math.min(target, d.pos + step);
          else d.pos = Math.max(target, d.pos - step);
          if (d.pos === target) d.travelling = false;
        } else {
          d.travelling = false;
        }
      }
    }
  }

  // ---- alarm derivation -----------------------------------------
  const tsReg = {};       // key -> first-seen Date
  const ackSet = new Set();
  const SEED = [
    { key:"S1", tag:"T657A", area:"chp", pri:"low",  ackd:true,  ago: 1000*60*42,
      desc: ml("Temperatura scarico CHP elevata — rientrata","CHP exhaust temperature high — cleared","Wysoka temperatura spalin CHP — ustąpił"), state:"cleared" },
    { key:"S2", tag:"CU721", area:"air", pri:"low",  ackd:false, ago: 1000*60*18,
      desc: ml("Compressore 2 — manutenzione programmata in scadenza","Compressor 2 — scheduled service due","Sprężarka 2 — zbliża się przegląd"), state:"active" },
    { key:"S3", tag:"PP530", area:"per", pri:"low",  ackd:true,  ago: 1000*60*120,
      desc: ml("Pompa percolato di riserva in stand-by","Standby percolate pump on standby","Pompa rezerwowa w trybie czuwania"), state:"active" },
  ];
  function ml(it,en,pl){ return {it,en,pl}; }

  function priFor(d, q) {
    const L = d.lim || {};
    if ((L.HH != null && d.v >= L.HH) || (L.LL != null && d.v <= L.LL)) return "high";
    return q === "warn" ? "med" : "high";
  }

  function compute() {
    const out = [];
    const now = Date.now();
    for (const d of D) {
      let key = null, pri = null, desc = null;
      if (d.kind === "instrument") {
        const q = qualOf(d);
        if (q === "alarm" || q === "warn") {
          key = d.id + "_" + q;
          pri = priFor(d, q);
          const dir = (d.lim.H != null && d.v >= d.lim.H) || (d.lim.HH != null && d.v >= d.lim.HH) ? "▲" : "▼";
          desc = { it: d.desc.it + " " + dir, en: d.desc.en + " " + dir, pl: d.desc.pl + " " + dir };
        }
      } else if (d.kind === "motor" && d.fault) {
        key = d.id + "_flt"; pri = "high";
        desc = { it: d.desc.it + " — guasto", en: d.desc.en + " — fault", pl: d.desc.pl + " — awaria" };
      } else if (d.kind === "valve" && d.fault) {
        key = d.id + "_flt"; pri = "high";
        desc = { it: d.desc.it + " — guasto manovra", en: d.desc.en + " — move fault", pl: d.desc.pl + " — awaria ruchu" };
      }
      if (key) {
        if (!tsReg[key]) tsReg[key] = new Date(now - Math.random() * 1000 * 60 * 30);
        out.push({ key, tag: d.tag, area: d.area, devId: d.id, pri, desc, ts: tsReg[key], state: "active", ackd: ackSet.has(key) });
      }
    }
    for (const s of SEED) {
      if (!tsReg[s.key]) tsReg[s.key] = new Date(now - s.ago);
      out.push({ key: s.key, tag: s.tag, area: s.area, pri: s.pri, desc: s.desc, ts: tsReg[s.key], state: s.state, ackd: s.ackd || ackSet.has(s.key) });
    }
    // sort: active+unack first, then by priority then time desc
    const prw = { high: 0, med: 1, low: 2 };
    out.sort((a, b) => (a.ackd - b.ackd) || (prw[a.pri] - prw[b.pri]) || (b.ts - a.ts));
    return out;
  }

  function ack(key) { ackSet.add(key); SEED.forEach(s => { if (s.key === key) s.ackd = true; }); }
  function ackAll() { compute().forEach(a => ackSet.add(a.key)); SEED.forEach(s => s.ackd = true); }

  window.TICK_MS = 1000;
  window.qualOf = qualOf;
  window.simTick = tick;
  window.computeAlarms = compute;
  window.ackAlarm = ack;
  window.ackAllAlarms = ackAll;
})();

// ---- formatting helpers (global) --------------------------------
function fmtNum(v, dp) {
  if (v == null || isNaN(v)) return "—";
  return Number(v).toLocaleString("en-US", { minimumFractionDigits: dp || 0, maximumFractionDigits: dp || 0 });
}
function fmtClock(d) {
  return d.toLocaleTimeString("it-IT", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
}
window.fmtNum = fmtNum;
window.fmtClock = fmtClock;
