// INVYONE Stage-2 Water Plant Topology
// Port-based connection model: pipes reference `componentId.portName` instead of raw coords.
// Canvas: 2000 x 880 SVG.

(function (global) {
  // ===== TANKS =====
  const TANKS = [
    { id: 'sand',          x: 340,  y: 90,  w: 96,  h: 150, type: 'ac',     label: 'SAND TANK',         capacity: 5000,  level: 62.0,  color: '#5af9ff', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
    { id: 'ac',            x: 500,  y: 90,  w: 96,  h: 150, type: 'ac',     label: 'AC TANK',           capacity: 5000,  level: 58.4,  color: '#5af9ff', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
    { id: 'raw',           x: 661,  y: 51,  w: 120, h: 230, type: 'tank',   label: 'RAW WATER TANK',    capacity: 30000, level: 75.0,  color: '#3aa3ff', alarms: { hh: 95, hl: 85, ll: 20, lh: 10 } },
    { id: 'chem',          x: 895,  y: 80,  w: 50,  h: 70,  type: 'dosing', label: 'Chem',              capacity: 500,   level: 71.0,  color: '#7cff3a', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
    { id: 'water-filter',  x: 1600, y: 60,  w: 120, h: 230, type: 'tank',   label: 'WATER FILTER TANK', capacity: 25000, level: 48.0,  color: '#3aa3ff', alarms: { hh: 95, hl: 85, ll: 20, lh: 10 }, nozzles: [{ side: 'left', pos: 0.30 }, { side: 'bot', pos: 0.5 }] },
    { id: 'chem-cep-1',    x: 60,   y: 545, w: 50,  h: 70,  type: 'dosing', label: 'CEP-1',             capacity: 500,   level: 64.2,  color: '#ff8a3a', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
    { id: 'chem-cep-2',    x: 130,  y: 545, w: 50,  h: 70,  type: 'dosing', label: 'CEP-2',             capacity: 500,   level: 55.8,  color: '#ff4f9a', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
    { id: 'cip-tank',      x: 80,   y: 845, w: 60,  h: 80,  type: 'dosing', label: 'CIP TANK',          capacity: 1500,  level: 80.5,  color: '#5af9ff', alarms: { hh: 95, hl: 85, ll: 15, lh: 5 } },
    { id: 'mbr',           x: 760,  y: 600, w: 500, h: 200, type: 'mbr',    label: 'MBR TANK',          capacity: 40000, level: 52.0,  color: '#ffd682', alarms: { hh: 92, hl: 80, ll: 20, lh: 10 } },
    { id: 'dp',            x: 1320, y: 545, w: 90,  h: 180, type: 'tank',   label: 'DP TANK',           capacity: 8000,  level: 87.0,  color: '#3aa3ff', alarms: { hh: 95, hl: 85, ll: 20, lh: 10 } },
    { id: 'regulating',    x: 1580, y: 640, w: 100, h: 180, type: 'tank',   label: 'REGULATING TANK',   capacity: 12000, level: 54.06, color: '#ff8a3a', alarms: { hh: 95, hl: 85, ll: 20, lh: 10 } },
  ];

  const FILTERS = [
    { id: 'pump-filter', anchor: { to: 'bw-a1.out',    myPort: 'left', dx: 107 },             label: 'PUMP FILTER' },
    { id: 'uf-filter',   anchor: { to: 'uf-pump-a.out',myPort: 'left', dx: 50 },              label: 'FILTER' },
    { id: 'ro-filter',   anchor: { to: 'ro-feed.out',  myPort: 'left', dx: 50 },              label: 'FILTER' },
    { id: 'cip-filter',  anchor: { to: 'cip-tank.right', myPort: 'left', dx: 35, dy: 1 },     label: 'FILTER' },
  ];

  const MODULES = [
    { id: 'uf-system',  anchor: { to: 'uf-filter.right', myPort: 'topLeft', dx: 82, dy: -87 }, type: 'membrane', count: 5, label: 'UF SYSTEM',  accent: '#5af9ff', orientation: 'vertical' },
    { id: 'ro-system',  anchor: { to: 'ro-press.out',    myPort: 'in',      dx: 55, dy: -47 },  type: 'membrane', count: 5, label: 'RO SYSTEM',  accent: '#5af9ff', orientation: 'horizontal' },
    { id: 'mbr-carb-l', anchor: { to: 'mbr.top',         myPort: 'topLeft', dx: -183, dy: 106 }, type: 'carbon', count: 5, label: 'CARBON L',   accent: '#5af9ff' },
    { id: 'mbr-carb-r', anchor: { to: 'mbr.top',         myPort: 'topLeft', dx: 77,  dy: 106 },  type: 'carbon', count: 5, label: 'CARBON R',   accent: '#5af9ff' },
  ];

  const PUMPS = [
    // Root pumps (no natural upstream tank — keep absolute)
    { id: 'bw-a1',      x: 78, y: 190, label: 'FEED-A', source: null, dest: 'sand', lpm: 80, valves: ['v-input'], pipes: ['p-input-bwa1', 'p-bwa1-pf', 'p-pf-sand'] },
    { id: 'air-blower', x: 380, y: 865, label: 'AIR BLOWER', source: null, dest: 'mbr', lpm: 0, valves: [], pipes: ['p-blower-mbr'] },

    // Tank-foot pumps (anchor to tank bottom, my top inlet aligns)
    { id: 'sand-out', anchor: { to: 'sand.bot', myPort: 'in', dy: 14 }, label: 'SAND PUMP', source: 'sand', dest: 'ac',  lpm: 80, valves: ['v-sand-out'], pipes: ['p-sand-down', 'p-sand-ac'], orient: 'v' },
    { id: 'ac-out',   anchor: { to: 'ac.bot',   myPort: 'in', dy: 14 }, label: 'AC PUMP',   source: 'ac',   dest: 'raw', lpm: 80, valves: ['v-ac-out'],   pipes: ['p-ac-down', 'p-ac-raw'],   orient: 'v' },
    { id: 'chem-uf',  anchor: { to: 'chem.bot', myPort: 'in', dy: 90 }, label: 'CHEM PUMP', source: 'chem', dest: 'water-filter', lpm: 8, valves: ['v-chem-uf'], pipes: ['p-chem-uf', 'p-chemuf-out'], orient: 'v' },

    // Process pumps (anchor to upstream tank or peer pump)
    { id: 'bw-a2',     anchor: { to: 'raw.right',     myPort: 'in', dx: 50, dy: 45 },  label: 'FEED-B', source: 'raw',          dest: 'water-filter', lpm: 120, valves: ['v-raw-out'],         pipes: ['p-raw-bw2', 'p-bw2-uf'] },
    { id: 'uf-pump-a', anchor: { to: 'bw-a2.out',     myPort: 'in', dx: 120 },         label: 'UF PUMP-A',  source: 'raw',          dest: 'water-filter', lpm: 200, valves: ['v-uf-in','v-uf-out'], pipes: ['p-raw-uf', 'p-uf-filter', 'p-filter-uf', 'p-uf-wf'] },
    { id: 'uf-pump-b', anchor: { to: 'uf-pump-a.bot', myPort: 'top', dy: 12 },         label: 'UF PUMP-B',  source: 'raw',          dest: 'water-filter', lpm: 200, valves: ['v-uf-in','v-uf-out'], pipes: ['p-raw-uf', 'p-ufpb-filter', 'p-filter-uf', 'p-uf-wf'] },

    // CEP/CIP pumps — root for that area (no natural anchor target before them)
    { id: 'ro-feed',   x: 210,  y: 630, label: 'RO FEED',  source: 'water-filter', dest: 'mbr', lpm: 150, valves: ['v-ro-in'],        pipes: ['p-wf-ro', 'p-rofeed-filter'] },
    { id: 'ro-press',  anchor: { to: 'ro-filter.right', myPort: 'in', dx: 13 }, label: 'RO PRESS', source: 'water-filter', dest: 'mbr', lpm: 150, valves: ['v-ro-press-out'], pipes: ['p-filter-ropress', 'p-ropress-ro', 'p-ro-mbr'] },
    { id: 'cip-pump',  anchor: { to: 'cip-filter.right', myPort: 'in', dx: 60, dy: 14 }, label: 'CIP PUMP', source: 'cip-tank', dest: 'ro-system', lpm: 60, valves: ['v-cip-out'], pipes: ['p-ciptank-pump', 'p-cip-ro'] },

    // Downstream pumps
    { id: 'aer-drain', anchor: { to: 'mbr.botRight', myPort: 'in', dx: -152, dy: 0 },  label: 'AERATION DRAIN', source: 'mbr', dest: 'dp', lpm: 100, valves: ['v-mbr-out'], pipes: ['p-mbr-aer', 'p-aer-dp'] },
    { id: 'dp-out',    anchor: { to: 'dp.right',     myPort: 'in', dx: 80, dy: 155 },  label: 'DP PUMP', source: 'dp', dest: 'regulating', lpm: 100, valves: ['v-dp-out'], pipes: ['p-dp-down', 'p-dp-pump-reg'] },
  ];

  const VALVES = [
    // All valves placed on their gating pipe — onPipe + at fraction
    { id: 'v-input',        anchor: { onPipe: 'p-bwa1-pf',    at: 0.30 },   label: 'V-IN',     open: false },
    { id: 'v-sand-out',     anchor: { onPipe: 'p-sand-ac',    at: 0.55 },   label: '',         open: false },
    { id: 'v-ac-out',       anchor: { onPipe: 'p-ac-raw',     at: 0.78, dx: -46, dy: -1 },   label: '',         open: false },
    { id: 'v-raw-out',      anchor: { onPipe: 'p-raw-bw2',    at: 0.78 },   label: '',         open: false },
    { id: 'v-chem-uf',      anchor: { onPipe: 'p-chem-uf',    at: 0.5 },    label: '',         open: false },
    { id: 'v-uf-in',        anchor: { onPipe: 'p-bw2-uf',     at: 0.75 },   label: 'V-UF-IN',  open: false },
    { id: 'v-uf-out',       anchor: { onPipe: 'p-uf-wf',      at: 0.20, dx: -24, dy: 0 },   label: 'V-UF-OUT', open: false },
    { id: 'v-ro-in',        anchor: { onPipe: 'p-rofeed-filter', at: 0.5 }, label: '',         open: false },
    { id: 'v-ro-press-out', anchor: { onPipe: 'p-ropress-ro', at: 0.4 },    label: '',         open: false },
    { id: 'v-cip-out',      anchor: { onPipe: 'p-cip-ro',     at: 0.4 },    label: '',         open: false },
    { id: 'v-mbr-out',      anchor: { onPipe: 'p-mbr-aer',    at: 0.4 },    label: '',         open: false },
    { id: 'v-dp-out',       anchor: { onPipe: 'p-dp-down',    at: 0.5 },    label: '',         open: false },
  ];

  // ===== ANCHORS =====
  // Components can declare `anchor: { to: 'parent.port', myPort: 'in', dx?, dy? }`
  // Position is derived from parent — no hardcoded x/y needed for anchored components.
  function getPortAbsPos(comp, portName) {
    // Resolves a single port's absolute coords given a component def with x/y/w/h.
    const { x, y, w, h, type } = comp;
    const cw = w || 60, ch = h || 60;
    if (type === 'ac') {
      if (portName === 'bot')   return { x: x + cw / 2, y: y + ch + 6 };
      if (portName === 'top')   return { x: x + cw / 2, y };
      if (portName === 'left')  return { x: x,        y: y + ch / 2 + 5 };
      if (portName === 'right') return { x: x + cw,   y: y + ch / 2 + 5 };
    } else if (type === 'dosing') {
      if (portName === 'bot')   return { x: x + cw / 2, y: y + ch };
      if (portName === 'top')   return { x: x + cw / 2, y };
      if (portName === 'left')  return { x: x,        y: y + ch / 2 };
      if (portName === 'right') return { x: x + cw,   y: y + ch / 2 };
    } else if (type === 'mbr' || type === 'tank') {
      if (portName === 'bot')      return { x: x + cw / 2, y: y + ch };
      if (portName === 'top')      return { x: x + cw / 2, y };
      if (portName === 'botLeft')  return { x: x + 12,     y: y + ch };
      if (portName === 'botRight') return { x: x + cw - 12,y: y + ch };
      if (portName === 'left')     return { x: x,          y: y + ch / 2 };
      if (portName === 'right')    return { x: x + cw,     y: y + ch / 2 };
    } else if (type === 'pump') {
      // Horizontal pump
      if (portName === 'in')  return { x,          y: y + 30 };
      if (portName === 'out') return { x: x + 60,  y: y + 30 };
      if (portName === 'top') return { x: x + 30,  y };
      if (portName === 'bot') return { x: x + 30,  y: y + 60 };
    } else if (type === 'pumpV') {
      if (portName === 'in')  return { x: x + 30,  y };
      if (portName === 'out') return { x: x + 60,  y: y + 47 };
      if (portName === 'top') return { x: x + 30,  y };
      if (portName === 'bot') return { x: x + 30,  y: y + 70 };
    } else if (type === 'airBlower') {
      if (portName === 'in')  return { x,          y: y + 25 };
      if (portName === 'out') return { x: x + 80,  y: y + 25 };
      if (portName === 'top') return { x: x + 22,  y };
    } else if (type === 'valve') {
      // valve's own width is 50, body center at (25, 26)
      if (portName === 'left')   return { x,         y: y + 26 };
      if (portName === 'right')  return { x: x + 50, y: y + 26 };
      if (portName === 'center') return { x: x + 25, y: y + 26 };
      if (portName === 'top')    return { x: x + 25, y };
    } else if (type === 'filter') {
      if (portName === 'left')   return { x,         y: y + 41 };
      if (portName === 'right')  return { x: x + 32, y: y + 41 };
      if (portName === 'top')    return { x: x + 16, y };
      if (portName === 'bot')    return { x: x + 16, y: y + 82 };
      if (portName === 'center') return { x: x + 16, y: y + 41 };
    } else if (type === 'gauge') {
      if (portName === 'center') return { x: x + 40, y: y + 40 };
      if (portName === 'top')    return { x: x + 40, y };
      if (portName === 'bot')    return { x: x + 40, y: y + 75 };
    } else if (type === 'sensor') {
      if (portName === 'topLeft')  return { x,          y };
      if (portName === 'left')     return { x,          y: y + 18 };
      if (portName === 'right')    return { x: x + 110, y: y + 18 };
      if (portName === 'top')      return { x: x + 55, y };
      if (portName === 'center')   return { x: x + 55, y: y + 18 };
    } else if (type === 'led') {
      if (portName === 'center')   return { x: x + 7,  y: y + 7 };
    } else if (type === 'membraneV') {
      // vertical UF: w computed elsewhere; use defaults if no w
      const w = comp.w || 174;
      const h = 156;
      if (portName === 'topLeft')  return { x: x + 4,     y: y + 6 };
      if (portName === 'topRight') return { x: x + w - 4, y: y + 6 };
      if (portName === 'left')     return { x: x,         y: y + h / 2 };
      if (portName === 'right')    return { x: x + w,     y: y + h / 2 };
    } else if (type === 'membraneH') {
      const w = comp.w || 208;
      const h = comp.h || 136;
      if (portName === 'in')   return { x,         y: y + h / 2 };
      if (portName === 'out')  return { x: x + w,  y: y + h / 2 };
      if (portName === 'top')  return { x: x + w / 2, y };
      if (portName === 'bot')  return { x: x + w / 2, y: y + h };
    } else if (type === 'carbon') {
      const w = comp.w || 86;
      if (portName === 'topLeft')  return { x: x + 2,     y: y + 16 };
      if (portName === 'topRight') return { x: x + w - 2, y: y + 16 };
    }
    return null;
  }

  // Tag effective type on every component (so getPortAbsPos can look up).
  function tagTypes() {
    TANKS.forEach(t => { t._type = t.type; });
    PUMPS.forEach(p => {
      p._type = p.id === 'air-blower' ? 'airBlower' : (p.orient === 'v' ? 'pumpV' : 'pump');
    });
    VALVES.forEach(v => { v._type = 'valve'; });
    FILTERS.forEach(f => { f._type = 'filter'; });
    GAUGES.forEach(g => { g._type = 'gauge'; });
    SENSORS.forEach(s => { s._type = 'sensor'; });
    LEDS.forEach(l => { l._type = 'led'; });
    MODULES.forEach(m => {
      m._type = m.type === 'carbon' ? 'carbon' : (m.orientation === 'vertical' ? 'membraneV' : 'membraneH');
    });
  }

  // Walk an orthogonal pipe's points and find absolute (x,y) at a given fraction (0..1) of total length.
  function pipePointAt(points, fraction) {
    fraction = Math.max(0, Math.min(1, fraction || 0));
    let total = 0;
    for (let i = 1; i < points.length; i++) {
      total += Math.abs(points[i][0] - points[i - 1][0]) + Math.abs(points[i][1] - points[i - 1][1]);
    }
    const target = total * fraction;
    let acc = 0;
    for (let i = 1; i < points.length; i++) {
      const dx = points[i][0] - points[i - 1][0];
      const dy = points[i][1] - points[i - 1][1];
      const len = Math.abs(dx) + Math.abs(dy);
      if (acc + len >= target) {
        const t = len === 0 ? 0 : (target - acc) / len;
        return {
          x: points[i - 1][0] + dx * t,
          y: points[i - 1][1] + dy * t,
          horizontal: dx !== 0,
        };
      }
      acc += len;
    }
    return { x: points[points.length - 1][0], y: points[points.length - 1][1], horizontal: true };
  }

  // ===== ANCHOR RESOLUTION (multi-pass) =====
  function resolveAnchors() {
    tagTypes();
    const allComps = [...TANKS, ...PUMPS, ...VALVES, ...FILTERS, ...MODULES, ...GAUGES, ...SENSORS, ...LEDS];
    // Tanks/modules/panels with explicit x,y are pre-resolved.
    allComps.forEach(c => { if (typeof c.x === 'number' && typeof c.y === 'number' && !c.anchor) c._resolved = true; });

    let pass = 0, changed = true;
    while (changed && pass++ < 12) {
      changed = false;
      allComps.forEach(c => {
        if (c._resolved) return;
        if (!c.anchor) return;
        // onPipe handled in second phase
        if (c.anchor.onPipe) return;
        if (c.anchor.absolute) {
          c.x = c.anchor.absolute.x;
          c.y = c.anchor.absolute.y;
          c._resolved = true; changed = true; return;
        }
        if (!c.anchor.to) return;
        const [refId, refPort] = c.anchor.to.split('.');
        const refComp = allComps.find(x => x.id === refId);
        if (!refComp) { console.warn('[INVYONE] anchor ref missing:', c.anchor.to, 'for', c.id); return; }
        if (!refComp._resolved) return; // wait
        const refPos = getPortAbsPos({ ...refComp, type: refComp._type }, refPort);
        if (!refPos) { console.warn('[INVYONE] port not found:', c.anchor.to); return; }
        const myLocal = getPortAbsPos({ x: 0, y: 0, type: c._type, w: c.w, h: c.h }, c.anchor.myPort || 'left') || { x: 0, y: 0 };
        c.x = Math.round(refPos.x - myLocal.x + (c.anchor.dx || 0));
        c.y = Math.round(refPos.y - myLocal.y + (c.anchor.dy || 0));
        c._resolved = true;
        changed = true;
      });
    }
  }

  function resolveOnPipeAnchors(resolvedPipes) {
    const allComps = [...VALVES, ...FILTERS, ...GAUGES, ...SENSORS, ...LEDS];
    allComps.forEach(c => {
      if (c._resolved) return;
      if (!c.anchor || !c.anchor.onPipe) return;
      const pipeRes = resolvedPipes.find(p => p.spec.id === c.anchor.onPipe);
      if (!pipeRes) { console.warn('[INVYONE] onPipe ref missing:', c.anchor.onPipe); return; }
      const pt = pipePointAt(pipeRes.points, c.anchor.at != null ? c.anchor.at : 0.5);
      const myLocal = getPortAbsPos({ x: 0, y: 0, type: c._type, w: c.w, h: c.h }, c.anchor.myPort || 'center') || { x: 0, y: 0 };
      c.x = Math.round(pt.x - myLocal.x + (c.anchor.dx || 0));
      c.y = Math.round(pt.y - myLocal.y + (c.anchor.dy || 0));
      c._orientation = pt.horizontal ? 'h' : 'v';
      c._resolved = true;
    });
  }

  // ===== PORTS =====
  // Each component exposes named connection points. Pipes reference them as 'id.port'.
  function computePorts() {
    const ports = {};

    TANKS.forEach(t => {
      const { id, x, y, w, h, type } = t;
      if (type === 'ac') {
        ports[id] = {
          top:    { x: x + w / 2, y: y },
          left:   { x: x,         y: y + h / 2 + 5 },
          right:  { x: x + w,     y: y + h / 2 + 5 },
          bot:    { x: x + w / 2, y: y + h + 6 },
        };
      } else if (type === 'dosing') {
        ports[id] = {
          top:   { x: x + w / 2, y: y },
          bot:   { x: x + w / 2, y: y + h },
          left:  { x: x,         y: y + h / 2 },
          right: { x: x + w,     y: y + h / 2 },
        };
      } else if (type === 'mbr') {
        ports[id] = {
          top:      { x: x + w / 2, y: y },
          bot:      { x: x + w / 2, y: y + h },
          botLeft:  { x: x + 8,     y: y + h },
          botRight: { x: x + w - 8, y: y + h },
          left:     { x: x,         y: y + h / 2 },
          right:    { x: x + w,     y: y + h / 2 },
          rightHigh:{ x: x + w,     y: y + h * 0.35 },
          rightLow: { x: x + w,     y: y + h * 0.85 },
          leftHigh: { x: x,         y: y + h * 0.35 },
          leftLow:  { x: x,         y: y + h * 0.85 },
        };
      } else {
        ports[id] = {
          top:       { x: x + w / 2, y: y },
          bot:       { x: x + w / 2, y: y + h },
          botLeft:   { x: x + 12,    y: y + h },
          botRight:  { x: x + w - 12,y: y + h },
          left:      { x: x,         y: y + h / 2 },
          leftHigh:  { x: x,         y: y + h * 0.30 },
          leftLow:   { x: x,         y: y + h * 0.85 },
          right:     { x: x + w,     y: y + h / 2 },
          rightHigh: { x: x + w,     y: y + h * 0.30 },
          rightLow:  { x: x + w,     y: y + h * 0.85 },
        };
      }
    });

    PUMPS.forEach(p => {
      if (p.id === 'air-blower') {
        ports[p.id] = {
          in:    { x: p.x,      y: p.y + 25 },
          out:   { x: p.x + 80, y: p.y + 25 },
          top:   { x: p.x + 22, y: p.y },
        };
      } else if (p.orient === 'v') {
        ports[p.id] = {
          in:    { x: p.x + 30, y: p.y },         // top inlet
          out:   { x: p.x + 60, y: p.y + 47 },    // right outlet (mid-housing)
          top:   { x: p.x + 30, y: p.y },
          bot:   { x: p.x + 30, y: p.y + 70 },
        };
      } else {
        ports[p.id] = {
          in:    { x: p.x,      y: p.y + 30 },
          out:   { x: p.x + 60, y: p.y + 30 },
          top:   { x: p.x + 30, y: p.y },
          bot:   { x: p.x + 30, y: p.y + 60 },
        };
      }
    });

    VALVES.forEach(v => {
      ports[v.id] = {
        left:  { x: v.x + 4,  y: v.y + 26 },
        right: { x: v.x + 46, y: v.y + 26 },
      };
    });

    FILTERS.forEach(f => {
      ports[f.id] = {
        left:  { x: f.x,      y: f.y + 41 },
        right: { x: f.x + 32, y: f.y + 41 },
        top:   { x: f.x + 16, y: f.y },
        bot:   { x: f.x + 16, y: f.y + 82 },
      };
    });

    MODULES.forEach(m => {
      const count = m.count || 5;
      if (m.type === 'membrane' && m.orientation === 'vertical') {
        const cellW = 22, gap = 10;
        const w = count * cellW + (count - 1) * gap + 24;
        const h = 130 + 26;
        ports[m.id] = {
          topLeft:  { x: m.x + 4,     y: m.y + 6 },
          topRight: { x: m.x + w - 4, y: m.y + 6 },
          topMid:   { x: m.x + w / 2, y: m.y + 6 },
          botLeft:  { x: m.x + 4,     y: m.y + h - 6 },
          botRight: { x: m.x + w - 4, y: m.y + h - 6 },
          left:     { x: m.x,         y: m.y + h / 2 },
          right:    { x: m.x + w,     y: m.y + h / 2 },
        };
      } else if (m.type === 'membrane') {
        const cellH = 16, gap = 8;
        const w = 180 + 28;
        const h = count * cellH + (count - 1) * gap + 24;
        ports[m.id] = {
          in:  { x: m.x,         y: m.y + h / 2 },
          out: { x: m.x + w,     y: m.y + h / 2 },
          top: { x: m.x + w / 2, y: m.y },
          bot: { x: m.x + w / 2, y: m.y + h },
        };
      } else if (m.type === 'carbon') {
        const cellW = 14, gap = 5;
        const w = count * cellW + (count - 1) * gap + 16;
        ports[m.id] = {
          topLeft:  { x: m.x + 2,     y: m.y + 16 },
          topRight: { x: m.x + w - 2, y: m.y + 16 },
        };
      }
    });

    return ports;
  }

  // ===== PIPE RESOLVER =====
  function resolveRef(ref, ports) {
    if (typeof ref === 'object' && 'x' in ref && 'y' in ref) return ref;
    const [id, portName] = String(ref).split('.');
    const p = ports[id] && ports[id][portName];
    if (!p) {
      console.warn('[INVYONE] Port not found:', ref);
      return { x: 0, y: 0 };
    }
    return p;
  }

  // Auto-route orthogonal between two points with optional waypoints.
  // Each waypoint is { x?, y? } or a port ref string. Missing axis inherits from previous.
  function autoRoute(from, to, via, hint, ports) {
    const pts = [from];
    let cur = from;
    (via || []).forEach(wp => {
      const w = typeof wp === 'string' ? resolveRef(wp, ports) : wp;
      const next = {
        x: ('x' in w) ? w.x : cur.x,
        y: ('y' in w) ? w.y : cur.y,
      };
      if (next.x !== cur.x && next.y !== cur.y) {
        pts.push({ x: cur.x, y: next.y }); // implicit corner
      }
      pts.push(next);
      cur = next;
    });
    if (cur.x !== to.x && cur.y !== to.y) {
      if (hint === 'v-first') pts.push({ x: cur.x, y: to.y });
      else                    pts.push({ x: to.x, y: cur.y });
    }
    pts.push(to);
    return pts.map(p => [p.x, p.y]);
  }

  function resolvePipe(spec, ports) {
    if (Array.isArray(spec.points)) return spec.points;
    const from = resolveRef(spec.from, ports);
    const to   = resolveRef(spec.to,   ports);
    return autoRoute(from, to, spec.via, spec.hint, ports);
  }

  // ===== PIPES (port-based) =====
  // Each pipe: { id, from: 'comp.port', to: 'comp.port', via?: [...], hint?: 'h-first'|'v-first' }
  const PIPES = [
    // Top row — pre-treatment
    { id: 'p-input-bwa1', from: { x: 40, y: 250 },           to: 'bw-a1.in' },
    { id: 'p-bwa1-pf',    from: 'bw-a1.out',                 to: 'pump-filter.left' },
    { id: 'p-pf-sand',    from: 'pump-filter.right',         to: 'sand.left', via: [{ y: 200 }] },
    { id: 'p-sand-down',  from: 'sand.bot',                  to: 'sand-out.top' },
    { id: 'p-sand-ac',    from: 'sand-out.out',              to: 'ac-out.in', via: [{ y: 279 }] },
    { id: 'p-ac-down',    from: 'ac.bot',                    to: 'ac-out.top' },
    { id: 'p-ac-raw',     from: 'ac-out.out',                to: 'raw.top', via: [{ x: 605 }, { y: 90 }] },
    { id: 'p-raw-bw2',    from: 'raw.right',                 to: 'bw-a2.in', via: [{ y: 211 }] },
    { id: 'p-bw2-uf',     from: 'bw-a2.out',                 to: 'uf-pump-a.in' },
    { id: 'p-chem-uf',    from: 'chem.bot',                  to: 'chem-uf.in', hint: 'v-first' },
    { id: 'p-chemuf-out', from: 'chem-uf.out',               to: { x: 980, y: 209 } },
    { id: 'p-raw-uf',     from: 'raw.rightLow',              to: 'uf-pump-b.in', via: [{ x: 999 }] },
    { id: 'p-uf-filter',  from: 'uf-pump-a.out',             to: 'uf-filter.left' },
    { id: 'p-ufpb-filter',from: 'uf-pump-b.out',             to: 'uf-filter.left' },
    { id: 'p-filter-uf',  from: 'uf-filter.right',           to: 'uf-system.topLeft' },
    { id: 'p-uf-wf',      from: 'uf-system.botRight',        to: 'water-filter.leftHigh', via: [{ x: 1490 }, { y: 130 }] },

    // Bottom row — RO / MBR / DP / REG
    { id: 'p-wf-ro',         from: 'water-filter.bot',       to: 'ro-feed.in', via: [{ y: 420 }, { x: 200 }, { y: 660 }] },
    { id: 'p-rofeed-filter', from: 'ro-feed.out',            to: 'ro-filter.left' },
    { id: 'p-filter-ropress',from: 'ro-filter.right',        to: 'ro-press.in' },
    { id: 'p-ropress-ro',    from: 'ro-press.out',           to: 'ro-system.in' },
    { id: 'p-ro-mbr',        from: 'ro-system.out',          to: 'mbr.leftHigh' },
    { id: 'p-cip-ro',        from: 'cip-pump.out',           to: 'ro-system.bot', via: [{ y: 834 }] },
    { id: 'p-ciptank-pump',  from: 'cip-tank.bot',           to: 'cip-pump.in', via: [{ y: 900 }] },
    { id: 'p-blower-mbr',    from: 'air-blower.out',         to: 'mbr.bot', via: [{ x: 700 }] },

    // MBR → AERATION DRAIN → DP TANK
    { id: 'p-mbr-aer',    from: 'mbr.botRight',              to: 'aer-drain.in' },
    { id: 'p-aer-dp',     from: 'aer-drain.out',             to: 'dp.bot',       via: [{ x: 1289 }, { y: 760 }, { x: 1365 }] },

    // DP TANK → DP PUMP → REGULATING
    { id: 'p-dp-down',    from: 'dp.bot',                    to: 'dp-out.in' },
    { id: 'p-dp-pump-reg',from: 'dp-out.out',                to: 'regulating.left', via: [{ y: 730 }] },
  ];

  const GAUGES = [
    { id: 'p-in-1',   anchor: { to: 'bw-a1.top',          myPort: 'center', dx: -23, dy: -110 }, label: 'P-IN',     max: 8,  unit: 'bar', source: 'bw-a1' },
    { id: 'p-in-2',   anchor: { to: 'bw-a1.top',          myPort: 'center', dx:  87, dy: -110 }, label: 'P-PF',     max: 8,  unit: 'bar', source: 'bw-a1' },
    { id: 'p-raw',    anchor: { to: 'raw.bot',            myPort: 'center', dx: -31, dy:  64  }, label: 'P-RAW',    max: 8,  unit: 'bar', source: 'bw-a2' },
    { id: 'p-uf-1',   anchor: { to: 'uf-pump-a.bot',      myPort: 'center', dx: 218, dy: 100  }, label: 'P-UF-IN',  max: 10, unit: 'bar', source: 'uf-pump-a' },
    { id: 'p-uf-2',   anchor: { to: 'uf-pump-a.bot',      myPort: 'center', dx: 497, dy: 100  }, label: 'P-UF-OUT', max: 10, unit: 'bar', source: 'uf-pump-a' },
    { id: 'p-wf',     anchor: { to: 'water-filter.right', myPort: 'center', dx:  90, dy: -45  }, label: 'P-WF',     max: 8,  unit: 'bar', source: 'bw-a2' },
    { id: 'p-cep-1',  anchor: { to: 'chem-cep-1.bot',     myPort: 'center', dx: -15, dy:  95  }, label: 'P1',       max: 8,  unit: 'bar', source: 'ro-feed' },
    { id: 'p-cep-2',  anchor: { to: 'chem-cep-2.bot',     myPort: 'center', dx:  -5, dy:  95  }, label: 'P2',       max: 8,  unit: 'bar', source: 'ro-feed' },
    { id: 'p-ro-in',  anchor: { to: 'ro-feed.top',        myPort: 'center', dx:  20, dy: -57  }, label: 'P-RO-IN',  max: 16, unit: 'bar', source: 'ro-feed' },
    { id: 'p-ro-out', anchor: { to: 'ro-press.top',       myPort: 'center', dx: 315, dy: -130 }, label: 'P-RO-OUT', max: 16, unit: 'bar', source: 'ro-press' },
    { id: 'p-dp-1',   anchor: { to: 'dp.top',             myPort: 'center', dx:  95, dy: -50  }, label: 'P-DP',     max: 8,  unit: 'bar', source: 'aer-drain' },
  ];

  const SENSORS = [
    { id: 'do',      anchor: { to: 'mbr.top',     myPort: 'topLeft', dx: -250, dy: -90 }, label: 'DO',      value: 8.5,  unit: 'ppm',  min: 4,  max: 12, alarms: { lo: 5, hi: 11 } },
    { id: 'temp',    anchor: { to: 'mbr.top',     myPort: 'topLeft', dx: -110, dy: -90 }, label: 'Temp',    value: 25.0, unit: 'C',    min: 0,  max: 50, alarms: { lo: 10, hi: 35 } },
    { id: 'tss',     anchor: { to: 'mbr.top',     myPort: 'topLeft', dx:   30, dy: -90 }, label: 'TSS',     value: 0.28, unit: 'mg/L', min: 0,  max: 5,  alarms: { lo: 0, hi: 2 } },
    { id: 'ph',      anchor: { to: 'mbr.top',     myPort: 'topLeft', dx:  170, dy: -90 }, label: 'pH',      value: 8.5,  unit: 'pH',   min: 0,  max: 14, alarms: { lo: 6.5, hi: 8.5 } },
    { id: 'tss-raw', anchor: { to: 'bw-a1.bot',   myPort: 'topLeft', dx:  -68, dy: 105 }, label: 'TSS-RAW', value: 1.42, unit: 'mg/L', min: 0, max: 5,  alarms: { lo: 0, hi: 3 } },
  ];

  const LEDS = [
    { id: 'sys-power', anchor: { absolute: { x: 40, y: 35 } },                                       label: 'PWR' },
    { id: 'input-led', anchor: { to: 'bw-a1.in',  myPort: 'center', dx: -31, dy: -21 },              label: 'IN' },
  ];

  const TILES = [
    { id: 'flow-in',  x: 1830, y: 520, label: 'FLOW IN',       value: '0.0', unit: 'L/min', w: 150, h: 64 },
    { id: 'flow-out', x: 1830, y: 594, label: 'FLOW OUT',      value: '0.0', unit: 'L/min', w: 150, h: 64 },
    { id: 'recovery', x: 1830, y: 668, label: 'RECOVERY',      value: '0.0', unit: '%',     w: 150, h: 64 },
    { id: 'alarms',   x: 1830, y: 742, label: 'ACTIVE ALARMS', value: '0',   unit: '',      w: 150, h: 64 },
  ];

  const PANELS = [
    { id: 'cep',       x: 40,  y: 510, w: 410, h: 300, label: 'CEP SYSTEM' },
    { id: 'cip',       x: 40,  y: 820, w: 470, h: 125, label: 'CIP SYSTEM' },
    { id: 'mbr-frame', x: 760, y: 600, w: 500, h: 200, label: '' },
  ];

  // Detect orthogonal crossings between resolved pipes.
  // Returns map: pipeId → [{x, y}, ...] of points where THIS pipe should jump (gap).
  // Convention: vertical pipe jumps over horizontal pipe (vertical gets gap).
  function detectCrossings(resolvedPipes) {
    const gapsByPipe = {};
    for (let i = 0; i < resolvedPipes.length; i++) {
      const A = resolvedPipes[i];
      for (let j = 0; j < resolvedPipes.length; j++) {
        if (i === j) continue;
        const B = resolvedPipes[j];
        for (let a = 1; a < A.points.length; a++) {
          const [ax1, ay1] = A.points[a - 1];
          const [ax2, ay2] = A.points[a];
          const aIsV = (ax1 === ax2 && ay1 !== ay2);
          if (!aIsV) continue;
          for (let b = 1; b < B.points.length; b++) {
            const [bx1, by1] = B.points[b - 1];
            const [bx2, by2] = B.points[b];
            const bIsH = (by1 === by2 && bx1 !== bx2);
            if (!bIsH) continue;
            // A vertical at x=ax1, y∈[ayMin..ayMax]; B horizontal at y=by1, x∈[bxMin..bxMax]
            const ayMin = Math.min(ay1, ay2), ayMax = Math.max(ay1, ay2);
            const bxMin = Math.min(bx1, bx2), bxMax = Math.max(bx1, bx2);
            if (ax1 > bxMin && ax1 < bxMax && by1 > ayMin && by1 < ayMax) {
              const id = A.spec.id;
              gapsByPipe[id] = gapsByPipe[id] || [];
              if (!gapsByPipe[id].some(g => g.x === ax1 && g.y === by1)) {
                gapsByPipe[id].push({ x: ax1, y: by1 });
              }
            }
          }
        }
      }
    }
    return gapsByPipe;
  }

  function buildScene() {
    const C = global.INVYONE_COMP;
    resolveAnchors();
    let ports = computePorts();
    let resolvedPipes = PIPES.map(p => ({ spec: p, points: resolvePipe(p, ports) }));
    resolveOnPipeAnchors(resolvedPipes);
    // Re-compute ports + pipes after on-pipe components are placed (in case anything depends)
    ports = computePorts();
    resolvedPipes = PIPES.map(p => ({ spec: p, points: resolvePipe(p, ports) }));
    const out = [];

    const gapsByPipe = detectCrossings(resolvedPipes);

    PANELS.forEach(p => out.push(C.panel(p)));
    resolvedPipes.forEach(rp => out.push(C.pipe({ id: rp.spec.id, points: rp.points, gaps: gapsByPipe[rp.spec.id] || [] })));

    TANKS.forEach(t => {
      if (t.type === 'ac') {
        out.push(C.acTank({ id: t.id, x: t.x, y: t.y, w: t.w, h: t.h, label: t.label, color: t.color }));
      } else if (t.type === 'dosing') {
        out.push(C.dosingTank({ id: t.id, x: t.x, y: t.y, label: t.label, color: t.color }));
      } else if (t.type === 'mbr') {
        // Aeration bubbles: 7 circles distributed across MBR width, animated when .aerating
        const bubbleSpots = [60, 130, 195, 250, 305, 370, 440];
        const bubbles = bubbleSpots.map((bx, i) => `<circle class="bubble" cx="${bx}" cy="${t.h - 10}" r="${1.6 + (i % 3) * 0.5}" fill="#5af9ff" opacity="0" style="animation-delay:${(i * 0.42).toFixed(2)}s"/>`).join('\n  ');
        out.push(`
<g class="comp comp-tank comp-tank-mbr" data-comp-id="${t.id}" data-comp-type="tank" transform="translate(${t.x} ${t.y})">
  <text x="${t.w / 2}" y="-12" class="comp-label" font-size="15" font-weight="700" text-anchor="middle">${t.label}</text>
  <rect class="tank-shell" x="0" y="0" width="${t.w}" height="${t.h}" rx="4" fill="url(#metal-tank)" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <rect x="3" y="3" width="${t.w - 6}" height="${t.h - 6}" fill="#071326" stroke="#aaa" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <clipPath id="tank-${t.id}-clip"><rect x="3" y="3" width="${t.w - 6}" height="${t.h - 6}"/></clipPath>
  <rect x="3" y="${t.h * 0.55}" width="${t.w - 6}" height="${t.h * 0.45 - 3}" fill="${t.color}" opacity="0.28" clip-path="url(#tank-${t.id}-clip)"/>
  <rect id="tank-${t.id}-liquid" x="3" y="${t.h - 3}" width="${t.w - 6}" height="0" fill="${t.color}" opacity="0.32" clip-path="url(#tank-${t.id}-clip)"/>
  <ellipse id="tank-${t.id}-surface" cx="${t.w / 2}" cy="${t.h - 3}" rx="${(t.w - 6) / 2 - 4}" ry="2.5" fill="${t.color}" opacity="0" clip-path="url(#tank-${t.id}-clip)"/>
  <g class="mbr-bubbles" pointer-events="none" clip-path="url(#tank-${t.id}-clip)">
    ${bubbles}
  </g>
  <rect id="tank-${t.id}-level-bg" x="${t.w / 2 - 32}" y="62" width="64" height="22" rx="3" fill="#000" stroke="#fff" stroke-width="1" vector-effect="non-scaling-stroke"/>
  <text id="tank-${t.id}-text" x="${t.w / 2}" y="78" class="tank-text" font-size="14" text-anchor="middle">0.0%</text>
</g>`);
      } else {
        out.push(C.tank({ id: t.id, x: t.x, y: t.y, w: t.w, h: t.h, label: t.label, color: t.color, nozzles: t.nozzles }));
      }
    });

    FILTERS.forEach(f => out.push(C.filter({ id: f.id, x: f.x, y: f.y, label: f.label })));
    MODULES.forEach(m => {
      if (m.type === 'membrane') out.push(C.membraneBank({ id: m.id, x: m.x, y: m.y, count: m.count, label: m.label, accent: m.accent, orientation: m.orientation }));
      else if (m.type === 'carbon') out.push(C.carbonBank({ id: m.id, x: m.x, y: m.y, count: m.count, label: m.label }));
    });
    PUMPS.forEach(p => {
      if (p.id === 'air-blower') out.push(C.airBlower({ id: p.id, x: p.x, y: p.y, label: p.label }));
      else if (p.orient === 'v')  out.push(C.pumpV({ id: p.id, x: p.x, y: p.y, label: p.label }));
      else                        out.push(C.pump({ id: p.id, x: p.x, y: p.y, label: p.label }));
    });
    VALVES.forEach(v => out.push(C.valve({ ...v, orientation: v._orientation || 'h' })));

    SENSORS.forEach(s => out.push(C.sensor(s)));
    GAUGES.forEach(g => out.push(C.pressureGauge(g)));
    LEDS.forEach(l => out.push(C.led(l)));
    TILES.forEach(t => out.push(C.statusTile(t)));
    return out.join('\n');
  }

  global.INVYONE_TOPO = { TANKS, PUMPS, VALVES, PIPES, FILTERS, MODULES, GAUGES, SENSORS, LEDS, TILES, PANELS, buildScene, computePorts, resolvePipe };
})(window);
