// INVYONE Stage-2 — Runtime Engine
// state + 250ms tick loop + flow/pressure/alarm 시뮬레이션
// PLC 연결 없는 데모용. 모든 컴포넌트가 독립 제어 가능 (샌드박스).

(function (global) {
  const TICK_MS = 250;
  const state = {
    tanks: {}, pumps: {}, valves: {}, sensors: {}, gauges: {}, cartridges: {},
    events: [], paused: false, flowMultiplier: 5.0, currentMode: 'STOP'
  };
  let tickId = null;
  let T = null;

  // ============================================================
  // INIT
  // ============================================================
  function init() {
    T = global.INVYONE_TOPO;
    T.TANKS.forEach(t => state.tanks[t.id] = Object.assign({}, t, { alarmState: null }));
    T.PUMPS.forEach(p => state.pumps[p.id] = Object.assign({}, p, { running: false, rpm: 1450, runtime: 0 }));
    T.VALVES.forEach(v => state.valves[v.id] = Object.assign({}, v));
    T.SENSORS.forEach(s => state.sensors[s.id] = Object.assign({}, s, { alarmState: null }));
    T.GAUGES.forEach(g => state.gauges[g.id] = Object.assign({}, g, { value: 0 }));
    T.MODULES.forEach(m => {
      if (m.type === 'carbon') {
        for (let i = 1; i <= m.count; i++) state.cartridges[`${m.id}-${i}`] = { isolated: false };
      }
    });
    Object.keys(state.tanks).forEach(setTankVisual);
    Object.keys(state.pumps).forEach(setPumpVisual);
    Object.keys(state.valves).forEach(setValveVisual);
    Object.keys(state.sensors).forEach(setSensorVisual);
    Object.keys(state.gauges).forEach(setGaugeVisual);
    updateRoutes();
    updateTiles();
    emitEvent('INVYONE Stage-2 데모 준비 완료', 'info');
  }

  // ============================================================
  // VISUAL UPDATERS
  // ============================================================
  function tankDef(id) { return T.TANKS.find(t => t.id === id); }

  function setTankVisual(id) {
    const t = state.tanks[id];
    if (!t) return;
    const def = tankDef(id);
    const pct = clamp(t.level, 0, 100);

    let yMin, yMax;
    if (def.type === 'ac')          { yMin = 26; yMax = def.h - 12; }  // cone(22)+pad(4) … h-skirt(8)-pad(4)
    else if (def.type === 'dosing') { yMin = 2;  yMax = def.h - 2; }
    else                            { yMin = 3;  yMax = def.h - 3; }
    const fullH = yMax - yMin;
    const liquidH = fullH * pct / 100;
    const yLiq = yMax - liquidH;

    const liq = qid(`tank-${id}-liquid`);
    if (liq) { liq.setAttribute('y', yLiq.toFixed(2)); liq.setAttribute('height', liquidH.toFixed(2)); }
    const surf = qid(`tank-${id}-surface`);
    if (surf) { surf.setAttribute('cy', yLiq.toFixed(2)); surf.style.opacity = pct > 0 ? 0.85 : 0; }
    const text = qid(`tank-${id}-text`);
    if (text) text.textContent = pct.toFixed(1) + '%';

    // Alarm classification
    const a = def.alarms || {};
    let alarm = null;
    if (a.hh != null && pct >= a.hh) alarm = 'hh';
    else if (a.hl != null && pct >= a.hl) alarm = 'hl';
    else if (a.ll != null && pct <= a.ll) alarm = 'll';
    else if (a.lh != null && pct <= a.lh) alarm = 'lh';
    if (t.alarmState !== alarm) {
      const prev = t.alarmState;
      t.alarmState = alarm;
      if (alarm) emitEvent(`${def.label}: ${alarm.toUpperCase()} 알람 (${pct.toFixed(1)}%)`, 'alarm');
      else if (prev) emitEvent(`${def.label}: 정상 복귀 (${pct.toFixed(1)}%)`, 'info');
    }
    const bg = qid(`tank-${id}-level-bg`);
    if (bg) {
      bg.setAttribute('fill', alarm === 'hh' || alarm === 'll' ? '#5a102c' : alarm === 'hl' || alarm === 'lh' ? '#4a3a10' : '#000');
      bg.setAttribute('stroke', alarm === 'hh' || alarm === 'll' ? '#ff4f9a' : alarm === 'hl' || alarm === 'lh' ? '#ff8a3a' : '#fff');
    }
    const root = qsel(`[data-comp-id="${id}"][data-comp-type="tank"]`);
    if (root) {
      root.classList.toggle('alarm-hh', alarm === 'hh');
      root.classList.toggle('alarm-hl', alarm === 'hl');
      root.classList.toggle('alarm-ll', alarm === 'll');
      root.classList.toggle('alarm-lh', alarm === 'lh');
    }
  }

  function setPumpVisual(id) {
    const p = state.pumps[id];
    if (!p) return;
    const root = qsel(`[data-comp-id="${id}"][data-comp-type="pump"]`);
    if (root) root.classList.toggle('running', p.running);
    const led = qid(`pump-${id}-led`);
    if (led) led.setAttribute('fill', p.running ? '#7cff3a' : '#2a3f5a');
  }

  function setValveVisual(id) {
    const v = state.valves[id];
    if (!v) return;
    const root = qsel(`[data-comp-id="${id}"][data-comp-type="valve"]`);
    if (root) {
      root.classList.toggle('open', !!v.open);
      root.classList.toggle('closed', !v.open);
    }
    // handle rotation driven by CSS .closed class
    const body = qid(`valve-${id}-body`);
    if (body) body.setAttribute('fill', v.open ? '#082a18' : '#071326');
  }

  function setSensorVisual(id) {
    const s = state.sensors[id];
    if (!s) return;
    const valEl = qid(`sensor-${id}-value`);
    if (valEl) valEl.textContent = (typeof s.value === 'number' ? s.value.toFixed(2) : s.value);
    let alarm = null;
    if (s.alarms) {
      if (s.value < s.alarms.lo) alarm = 'lo';
      else if (s.value > s.alarms.hi) alarm = 'hi';
    }
    if (s.alarmState !== alarm) {
      const prev = s.alarmState;
      s.alarmState = alarm;
      if (alarm) emitEvent(`센서 ${s.label}: ${alarm.toUpperCase()} 임계 (${typeof s.value === 'number' ? s.value.toFixed(2) : s.value} ${s.unit})`, 'alarm');
      else if (prev) emitEvent(`센서 ${s.label}: 정상 복귀`, 'info');
    }
    const root = qsel(`[data-comp-id="${id}"][data-comp-type="sensor"]`);
    if (root) root.classList.toggle('alarm', !!alarm);
  }

  function setGaugeVisual(id) {
    const g = state.gauges[id];
    if (!g) return;
    const max = g.max || 8;
    const pct = clamp((g.value / max) * 100, 0, 100);
    const angle = -120 + (240 * pct / 100);
    const needle = qid(`gauge-${id}-needle`);
    if (needle) needle.setAttribute('transform', `rotate(${angle.toFixed(2)} 40 40)`);
    const text = qid(`gauge-${id}-text`);
    if (text) text.textContent = g.value.toFixed(1);
    const bg = qid(`gauge-${id}-bg`);
    if (bg) bg.setAttribute('fill', pct > 75 ? '#3a0e1e' : pct > 50 ? '#3a2a0e' : '#081326');
    const root = qsel(`[data-comp-id="${id}"][data-comp-type="gauge"]`);
    if (root) root.classList.toggle('alarm', pct > 85);
  }

  // ============================================================
  // ROUTING / FLOW
  // ============================================================
  function pumpRouteActive(p) {
    if (!p.running) return false;
    for (const vid of (p.valves || [])) {
      const v = state.valves[vid];
      if (v && !v.open) return false;
    }
    if (p.source) {
      const src = state.tanks[p.source];
      if (!src || src.level <= 0.05) return false;
    }
    if (p.dest) {
      const dst = state.tanks[p.dest];
      if (dst && dst.level >= 99.95) return false;
    }
    return true;
  }

  // Track pump activation state across ticks for gradual pipe-fill animation.
  const prevPumpActive = {};
  const activePumpsForPipe = {};   // { pipeId: Set<pumpId> } — OR-accumulated activity
  const PIPE_FILL_DELAY = 120;     // ms between successive pipes in a pump's chain

  function activatePipesGradually(pumpId) {
    const pump = state.pumps[pumpId];
    if (!pump) return;
    (pump.pipes || []).forEach((pid, i) => {
      setTimeout(() => {
        // pump may have been turned OFF during the wait — re-check
        if (!pumpRouteActive(state.pumps[pumpId])) return;
        activePumpsForPipe[pid] = activePumpsForPipe[pid] || new Set();
        activePumpsForPipe[pid].add(pumpId);
        const flowLine = qid(`pipe-${pid}-flow`);
        const pipeRoot = qsel(`[data-comp-id="${pid}"][data-comp-type="pipe"]`);
        if (flowLine) { flowLine.classList.add('active'); flowLine.classList.remove('blocked'); }
        if (pipeRoot) pipeRoot.classList.add('active');
      }, i * PIPE_FILL_DELAY);
    });
  }

  function deactivatePipes(pumpId) {
    const pump = state.pumps[pumpId];
    if (!pump) return;
    (pump.pipes || []).forEach(pid => {
      if (activePumpsForPipe[pid]) {
        activePumpsForPipe[pid].delete(pumpId);
        if (activePumpsForPipe[pid].size === 0) {
          const flowLine = qid(`pipe-${pid}-flow`);
          const pipeRoot = qsel(`[data-comp-id="${pid}"][data-comp-type="pipe"]`);
          if (flowLine) flowLine.classList.remove('active');
          if (pipeRoot) pipeRoot.classList.remove('active');
        }
      }
    });
  }

  function updateRoutes() {
    // Reset every tick — clean slate, then recompute from authoritative state
    document.querySelectorAll('.valve-flow').forEach(el => el.classList.remove('active', 'blocked'));
    document.querySelectorAll('.pipe-flow').forEach(el => el.classList.remove('blocked'));

    // Compute the authoritative set of active pipes from current pump state
    const activePipeSet = new Set();
    const blockedPipeSet = new Set();
    Object.values(state.pumps).forEach(p => {
      if (pumpRouteActive(p)) {
        (p.pipes || []).forEach(pid => activePipeSet.add(pid));
      } else if (p.running) {
        (p.pipes || []).forEach(pid => blockedPipeSet.add(pid));
      }
    });

    // Sync activePumpsForPipe with reality — drop entries whose pumps are no longer active
    Object.keys(activePumpsForPipe).forEach(pid => {
      const set = activePumpsForPipe[pid];
      [...set].forEach(pumpId => {
        if (!pumpRouteActive(state.pumps[pumpId])) set.delete(pumpId);
      });
    });

    // Drain pipes whose set went empty
    document.querySelectorAll('[data-comp-type="pipe"].active').forEach(el => {
      const id = el.getAttribute('data-comp-id');
      const set = activePumpsForPipe[id];
      if (!set || set.size === 0) {
        if (!activePipeSet.has(id)) {
          el.classList.remove('active');
          const flowLine = qid(`pipe-${id}-flow`);
          if (flowLine) flowLine.classList.remove('active');
        }
      }
    });

    // Per-pump triggers: detect new ON to start gradual fill
    Object.values(state.pumps).forEach(p => {
      const active = pumpRouteActive(p);
      const wasActive = !!prevPumpActive[p.id];
      if (active && !wasActive) activatePipesGradually(p.id);

      // valve flow follows pump active state (immediate)
      if (active) {
        (p.valves || []).forEach(vid => {
          const v = state.valves[vid];
          if (v && v.open) {
            const fl = qid(`valve-${vid}-flow-l`);
            const fr = qid(`valve-${vid}-flow-r`);
            if (fl) fl.classList.add('active');
            if (fr) fr.classList.add('active');
          }
        });
      }

      prevPumpActive[p.id] = active;
    });

    // Apply blocked state (running but route incomplete) — never on active pipes
    blockedPipeSet.forEach(pid => {
      if (activePipeSet.has(pid)) return;
      const flowLine = qid(`pipe-${pid}-flow`);
      if (flowLine) flowLine.classList.add('blocked');
    });

    // Module flow lines: if any pump route reaches them, mark active (heuristic by id matching)
    document.querySelectorAll('[data-comp-type="module"]').forEach(modEl => {
      const id = modEl.getAttribute('data-comp-id');
      let active = false;
      if (id === 'uf-system') {
        active = pumpRouteActive(state.pumps['uf-pump-a']) || pumpRouteActive(state.pumps['uf-pump-b']);
      } else if (id === 'mbr-carb-l' || id === 'mbr-carb-r') {
        // Carbon banks "active" while water flows through MBR (RO press feeding it OR aer-drain pulling it)
        active = pumpRouteActive(state.pumps['ro-press']) || pumpRouteActive(state.pumps['aer-drain']);
      } else if (id === 'ro-system') {
        active = pumpRouteActive(state.pumps['ro-feed']) || pumpRouteActive(state.pumps['ro-press']) || pumpRouteActive(state.pumps['cip-pump']);
      }
      modEl.classList.toggle('active', active);
      modEl.querySelectorAll('.flow').forEach(el => el.classList.toggle('active', active));
    });

    // MBR aeration: tank shell glows when AIR BLOWER is running
    const mbrEl = document.querySelector('[data-comp-id="mbr"][data-comp-type="tank"]');
    if (mbrEl) {
      const blower = state.pumps['air-blower'];
      mbrEl.classList.toggle('aerating', !!(blower && blower.running));
    }

    // Inline filters: active when any feed pump is delivering through them
    const filterFeed = {
      'pump-filter': ['bw-a1'],
      'uf-filter':   ['uf-pump-a', 'uf-pump-b'],
      'ro-filter':   ['ro-feed', 'ro-press'],
      'cip-filter':  ['cip-pump'],
    };
    Object.keys(filterFeed).forEach(fid => {
      const el = document.querySelector(`[data-comp-id="${fid}"][data-comp-type="filter"]`);
      if (!el) return;
      const active = filterFeed[fid].some(pid => {
        const pp = state.pumps[pid];
        return pp && pumpRouteActive(pp);
      });
      el.classList.toggle('active', active);
    });
  }

  // ============================================================
  // TICK LOOP
  // ============================================================
  function tick() {
    if (state.paused) return;

    Object.values(state.pumps).forEach(p => {
      if (!pumpRouteActive(p)) return;
      const lpm = (p.lpm || 0) * (p.rpm / 1450) * state.flowMultiplier;
      const liters = lpm / 60 * (TICK_MS / 1000);

      if (p.source) {
        const src = state.tanks[p.source];
        if (src) src.level = Math.max(0, src.level - (liters / src.capacity * 100));
      }
      if (p.dest) {
        const dst = state.tanks[p.dest];
        if (dst) dst.level = Math.min(100, dst.level + (liters / dst.capacity * 100));
      }
      p.runtime += TICK_MS / 1000;
    });

    Object.values(state.gauges).forEach(g => {
      // 시나리오가 게이지를 강제 설정한 경우 자동 갱신 skip (압력 알람값 보존)
      if (g.forced) return;
      const srcPump = g.source ? state.pumps[g.source] : null;
      let target = 0;
      if (srcPump && srcPump.running) {
        const ratio = pumpRouteActive(srcPump) ? 0.55 : 0.85; // blocked → higher pressure
        target = (g.max || 8) * ratio + (Math.random() - 0.5) * 0.2;
      } else {
        target = (Math.random() - 0.5) * 0.05;
      }
      g.value = g.value * 0.85 + target * 0.15;
      if (g.value < 0) g.value = 0;
    });

    Object.values(state.sensors).forEach(s => {
      if (typeof s.value !== 'number') return;
      // very gentle noise so manually-set values stay near their target
      const noise = (Math.random() - 0.5) * 0.0015 * (s.max - s.min);
      s.value = clamp(s.value + noise, s.min, s.max);
    });

    Object.keys(state.tanks).forEach(setTankVisual);
    Object.keys(state.gauges).forEach(setGaugeVisual);
    Object.keys(state.sensors).forEach(setSensorVisual);
    updateModuleReadouts();
    updateTiles();
    updateRoutes();
  }

  function updateModuleReadouts() {
    const ufIn  = state.gauges['p-uf-1'];
    const ufOut = state.gauges['p-uf-2'];
    const tmpEl = qid('module-uf-system-tmp');
    if (tmpEl) {
      const tmp = (ufIn && ufOut) ? Math.max(0, ufIn.value - ufOut.value) : 0;
      tmpEl.textContent = `TMP ${tmp.toFixed(2)} bar`;
    }
    const ufModeEl = qid('module-uf-system-mode');
    const ufRoot = qsel('[data-comp-id="uf-system"][data-comp-type="module"]');
    if (ufModeEl) {
      const m = state.currentMode;
      let modeText = 'STANDBY', color = '#7cff3a';
      if (m === 'NORMAL')        { modeText = 'FILTRATION'; color = '#5af9ff'; }
      else if (m === 'BACKWASH') { modeText = 'BACKWASH';   color = '#ff8a3a'; }
      else if (m === 'CIP')      { modeText = 'CIP';        color = '#ff4f9a'; }
      else if (m === 'CHEM-DOSE'){ modeText = 'CHEM DOSE';  color = '#7cff3a'; }
      ufModeEl.textContent = `▶ ${modeText}`;
      ufModeEl.setAttribute('fill', color);
    }
    if (ufRoot) {
      ufRoot.classList.toggle('mode-backwash', state.currentMode === 'BACKWASH');
      ufRoot.classList.toggle('mode-cip',      state.currentMode === 'CIP');
    }
  }

  function updateTiles() {
    let flowIn = 0, flowOut = 0, alarmCount = 0;
    Object.values(state.pumps).forEach(p => {
      if (pumpRouteActive(p)) {
        const lpm = (p.lpm || 0) * (p.rpm / 1450);
        if (!p.source) flowIn += lpm;
        if (p.dest === 'regulating') flowOut += lpm;
      }
    });
    Object.values(state.tanks).forEach(t => { if (t.alarmState) alarmCount++; });
    Object.values(state.sensors).forEach(s => { if (s.alarmState) alarmCount++; });
    setTileVal('flow-in', flowIn.toFixed(1));
    setTileVal('flow-out', flowOut.toFixed(1));
    setTileVal('recovery', flowIn > 0 ? clamp(flowOut / flowIn * 100, 0, 100).toFixed(1) : '0.0');
    setTileVal('alarms', String(alarmCount));
    const alarmsTile = qsel('[data-comp-id="alarms"][data-comp-type="tile"]');
    if (alarmsTile) alarmsTile.classList.toggle('alarm', alarmCount > 0);
  }
  function setTileVal(id, v) { const el = qid(`tile-${id}-value`); if (el) el.textContent = v; }

  // ============================================================
  // EVENTS / LOG
  // ============================================================
  function emitEvent(msg, level) {
    level = level || 'info';
    const time = new Date().toLocaleTimeString('ko-KR', { hour12: false });
    state.events.unshift({ time, msg, level });
    if (state.events.length > 200) state.events.pop();
    const log = document.getElementById('event-log');
    if (!log) return;
    const line = document.createElement('div');
    line.className = `event-line event-${level}`;
    line.textContent = `[${time}] ${msg}`;
    log.insertBefore(line, log.firstChild);
    while (log.children.length > 200) log.removeChild(log.lastChild);
  }

  // ============================================================
  // PUBLIC ACTIONS
  // ============================================================
  function togglePump(id) {
    const p = state.pumps[id]; if (!p) return;
    p.running = !p.running; setPumpVisual(id); updateRoutes();
    emitEvent(`PUMP ${p.label || id}: ${p.running ? 'ON' : 'OFF'}`, p.running ? 'info' : 'warn');
  }
  function toggleValve(id) {
    const v = state.valves[id]; if (!v) return;
    v.open = !v.open; setValveVisual(id); updateRoutes();
    emitEvent(`VALVE ${v.label || id}: ${v.open ? 'OPEN' : 'CLOSED'}`, 'info');
  }
  function setTankLevel(id, pct) {
    const t = state.tanks[id]; if (!t) return;
    t.level = clamp(pct, 0, 100); setTankVisual(id);
  }
  function setSensor(id, value) {
    const s = state.sensors[id]; if (!s) return;
    s.value = value; setSensorVisual(id);
  }
  function setRpm(id, rpm) {
    const p = state.pumps[id]; if (!p) return;
    p.rpm = clamp(rpm, 0, 3000);
  }
  function isolateCartridge(cid) {
    const c = state.cartridges[cid]; if (!c) return;
    c.isolated = !c.isolated;
    const el = document.getElementById(`cartridge-${cid}`);
    if (el) el.classList.toggle('isolated', c.isolated);
    emitEvent(`카트리지 ${cid}: ${c.isolated ? '격리(BYPASS)' : '복귀'}`, c.isolated ? 'warn' : 'info');
  }

  function applyMode(mode) {
    state.currentMode = mode;
    const setVO = (vid, open) => { if (state.valves[vid]) state.valves[vid].open = open; };
    const setPR = (pid, run)  => { if (state.pumps[pid])  state.pumps[pid].running = run; };

    if (mode === 'NORMAL') {
      ['v-input','v-sand-out','v-ac-out','v-raw-out','v-uf-in','v-uf-out','v-ro-in','v-ro-press-out','v-mbr-out','v-dp-out']
        .forEach(v => setVO(v, true));
      ['v-chem-uf','v-cip-out'].forEach(v => setVO(v, false));
      ['bw-a1','sand-out','ac-out','bw-a2','uf-pump-a','ro-feed','ro-press','aer-drain','dp-out','air-blower']
        .forEach(p => setPR(p, true));
      ['uf-pump-b','cip-pump','chem-uf'].forEach(p => setPR(p, false));
    } else if (mode === 'BACKWASH') {
      Object.keys(state.valves).forEach(v => setVO(v, false));
      Object.keys(state.pumps).forEach(p => setPR(p, false));
      ['v-input','v-uf-in','v-uf-out'].forEach(v => setVO(v, true));
      ['bw-a1','bw-a2'].forEach(p => setPR(p, true));
    } else if (mode === 'CIP') {
      Object.keys(state.valves).forEach(v => setVO(v, false));
      Object.keys(state.pumps).forEach(p => setPR(p, false));
      ['v-cip-out','v-ro-in'].forEach(v => setVO(v, true));
      ['cip-pump'].forEach(p => setPR(p, true));
    } else if (mode === 'CHEM-DOSE') {
      // start chemical dosing into UF
      setVO('v-chem-uf', true);
      setPR('chem-uf', true);
    } else if (mode === 'STOP') {
      Object.keys(state.valves).forEach(v => setVO(v, false));
      Object.keys(state.pumps).forEach(p => setPR(p, false));
    }
    Object.keys(state.valves).forEach(setValveVisual);
    Object.keys(state.pumps).forEach(setPumpVisual);
    updateRoutes();
    updateModuleReadouts();
    emitEvent(`운전모드 변경: ${mode}`, 'info');
    const modeEl = document.getElementById('current-mode');
    if (modeEl) modeEl.textContent = mode;
  }

  function reset() {
    T.TANKS.forEach(t => state.tanks[t.id].level = t.level);
    Object.keys(state.tanks).forEach(setTankVisual);
    applyMode('STOP');
    state.events.length = 0;
    const log = document.getElementById('event-log');
    if (log) log.innerHTML = '';
    emitEvent('SYSTEM RESET — 초기 데모 상태로 복구', 'info');
  }

  function triggerDemoAlarm() {
    state.tanks['raw'].level = 96;
    state.tanks['water-filter'].level = 4;
    state.sensors['ph'].value = 9.2;
    setTankVisual('raw');
    setTankVisual('water-filter');
    setSensorVisual('ph');
    emitEvent('데모 알람 트리거: RAW HH + WATER FILTER LL + pH HI', 'alarm');
  }

  function start() { if (!tickId) tickId = setInterval(tick, TICK_MS); }
  function stop()  { if (tickId) { clearInterval(tickId); tickId = null; } }

  // ============================================================
  // HELPERS
  // ============================================================
  function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
  function qid(id) { return document.getElementById(id); }
  function qsel(s) { return document.querySelector(s); }

  global.INVYONE_ENGINE = {
    state, init, start, stop, tick,
    togglePump, toggleValve, setTankLevel, setSensor, setRpm, isolateCartridge,
    applyMode, reset, triggerDemoAlarm, emitEvent,
    setTankVisual, setPumpVisual, setValveVisual, setSensorVisual, setGaugeVisual,
    updateRoutes
  };
})(window);
