const { assetCategoryManager } = require('../../datasets/assetData');
const assetApiConfig = require('../configs/assetApiConfig.js');

class AssetMenu {
  constructor({ manager = assetCategoryManager, softwareType = null } = {}) {
    this.manager = manager;
    this.softwareType = softwareType;
    this.categories = this.manager
      .listCategories({ withMeta: true })
      .reduce((map, meta) => {
        map[meta.softwareType] = this.manager.getCategory(meta.softwareType);
        return map;
      }, {});
  }

  normalizeCategory(key) {
    const category = this.categories[key];
    if (!category) {
      return null;
    }

    const softwareType = category.softwareType || key;
    return {
      ...category,
      label: category.label || category.softwareType || key,
      suppliers: (category.suppliers || []).map((supplier) => ({
        ...supplier,
        id: supplier.id || supplier.name,
        types: (supplier.types || []).map((type) => ({
          ...type,
          id: type.id || type.name,
          models: (type.models || []).map((model) => {
            const id = model.id || model.name;
            // Enrich each model with a slim preview curve (or null) so the
            // editor wizard can draw a sparkline without a round-trip.
            const previewCurve = this.buildPreviewCurve(softwareType, id, model.name);
            return {
              ...model,
              id,
              units: model.units || [],
              previewCurve: previewCurve || null
            };
          })
        }))
      }))
    };
  }

  resolveCategoryForNode(nodeName) {
    const keys = Object.keys(this.categories);
    if (keys.length === 0) {
      return null;
    }

    if (this.softwareType && this.categories[this.softwareType]) {
      return this.softwareType;
    }

    if (nodeName) {
      const normalized = typeof nodeName === 'string' ? nodeName.toLowerCase() : nodeName;
      if (normalized && this.categories[normalized]) {
        return normalized;
      }
    }

    // Previously fell back to keys[0] (alphabetically first category),
    // which meant a softwareType mismatch silently showed the wrong asset
    // tree — e.g. rotatingMachine (softwareType='rotatingmachine') with
    // no matching registry file saw 'diffuser' models in the dropdown.
    // Return null so the menu renders empty and the operator sees a clear
    // 'No suppliers available' placeholder instead of a wrong category.
    console.warn(
      `[AssetMenu] No asset category matches softwareType='${this.softwareType}' or nodeName='${nodeName}'. ` +
      `Available categories: [${keys.join(', ')}]. Menu will render empty.`
    );
    return null;
  }

  getAllMenuData(nodeName) {
    const categoryKey = this.resolveCategoryForNode(nodeName);
    const selectedCategories = {};

    if (categoryKey && this.categories[categoryKey]) {
      selectedCategories[categoryKey] = this.normalizeCategory(categoryKey);
    }

    return {
      categories: selectedCategories,
      defaultCategory: categoryKey,
      apiConfig: {
        url: `${assetApiConfig.baseUrl}/apis/products/PLC/integration/`,
        headers: { ...assetApiConfig.headers }
      }
    };
  }

  // Client-side wizard layer: chips, combobox, spec strip, curve mini-chart.
  // Listens to change events on the hidden <select>s that wireEvents already
  // populates — so cascade/reset logic stays in one place.
  getVisualInjectionCode(nodeName) {
    return `
      // Asset wizard visuals for ${nodeName}
      (function injectAssetWizardCss() {
        const id = 'evolv-asset-wizard-css';
        if (document.getElementById(id)) return;
        const css = [
          // Asset wizard — tightened layout (smaller radius/padding, no
          // uppercase label transform, single-line chip text) so the strip
          // reads as a compact form control instead of a row of pill cards.
          '.evolv-asset-hidden-natives { position:absolute !important; left:-9999px !important; height:0 !important; overflow:hidden; }',
          '.evolv-asset-wizard { display:flex; flex-direction:column; gap:8px; margin:6px 0 4px 0; max-width:460px; }',
          '.evolv-asset-chips { display:flex; flex-wrap:wrap; gap:4px; align-items:center; }',
          '.evolv-asset-chip {',
          '  display:inline-flex; align-items:baseline; gap:6px;',
          '  border:1px solid #d0d0d0; border-radius:4px; background:#fff;',
          '  padding:3px 8px; cursor:pointer; user-select:none;',
          '  font:inherit; color:#333; height:26px; box-sizing:border-box;',
          '  transition:border-color 80ms ease-out, background 80ms ease-out;',
          '}',
          '.evolv-asset-chip:hover { border-color:#86bbdd; background:#f5fafd; }',
          '.evolv-asset-chip[aria-selected="true"] { border-color:#1F4E79; background:#eaf4fb; }',
          '.evolv-asset-chip[disabled] { opacity:0.5; cursor:not-allowed; }',
          '.evolv-asset-chip-icon { color:#607484; font-size:11px; align-self:center; }',
          '.evolv-asset-chip-text { display:inline-flex; align-items:baseline; gap:5px; line-height:1; }',
          '.evolv-asset-chip-label { font-size:11px; font-weight:normal; color:#888; letter-spacing:0; text-transform:none; }',
          '.evolv-asset-chip-label::after { content:":"; color:#bbb; margin-left:1px; }',
          '.evolv-asset-chip-value { font-size:12px; font-weight:600; color:#1F4E79; max-width:160px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }',
          '.evolv-asset-chip-value[data-empty="true"] { color:#aaa; font-weight:400; font-style:italic; }',
          '.evolv-asset-chip-sep { color:#bbb; font-size:13px; line-height:1; user-select:none; padding:0 2px; }',
          '.evolv-asset-combobox { display:flex; flex-direction:column; gap:4px; border:1px solid #d0d0d0; border-radius:3px; background:#fff; padding:6px; }',
          '.evolv-asset-combobox-search { width:100%; box-sizing:border-box; padding:5px 7px; border:1px solid #ccc; border-radius:3px; font:inherit; font-size:12px; }',
          '.evolv-asset-combobox-search:focus { outline:none; border-color:#1F4E79; box-shadow:0 0 0 2px rgba(31,78,121,0.15); }',
          '.evolv-asset-combobox-list { max-height:200px; overflow-y:auto; }',
          '.evolv-asset-combobox-option {',
          '  padding:5px 8px; cursor:pointer; border-radius:2px;',
          '  font-size:12px; color:#333;',
          '}',
          '.evolv-asset-combobox-option:hover,',
          '.evolv-asset-combobox-option.evolv-asset-combobox-option-active { background:#eaf4fb; color:#1F4E79; }',
          '.evolv-asset-combobox-empty { padding:5px 8px; color:#888; font-size:11px; font-style:italic; }',
          '.evolv-asset-summary { display:grid; grid-template-columns:1fr 220px; gap:10px; border:1px solid #e2e2e2; border-radius:3px; padding:8px 10px; background:#fafafa; align-items:center; }',
          '.evolv-asset-specs { font-size:11.5px; color:#333; display:flex; flex-direction:column; gap:2px; }',
          '.evolv-asset-spec-row { display:flex; gap:6px; }',
          '.evolv-asset-spec-key { color:#888; min-width:74px; }',
          '.evolv-asset-spec-val { color:#1F4E79; font-weight:600; }',
          '.evolv-asset-curve { width:220px; height:110px; }',
          '.evolv-asset-curve svg { width:100%; height:100%; display:block; }',
          '.evolv-asset-curve-empty { display:flex; align-items:center; justify-content:center; color:#aaa; font-size:11px; font-style:italic; text-align:center; }',
          '.evolv-asset-tag-row { margin-top:2px; align-items:center; }',
          '.evolv-asset-tag-row > label { width:110px; white-space:nowrap; }',
          '.evolv-asset-tag-row input[type=text] { width:auto !important; max-width:200px; min-width:140px; font-size:12px; padding:3px 6px; }',
          '@media (max-width:560px) {',
          '  .evolv-asset-chips { flex-direction:column; align-items:stretch; }',
          '  .evolv-asset-chip-sep { display:none; }',
          '  .evolv-asset-chip { width:100%; justify-content:flex-start; }',
          '  .evolv-asset-summary { grid-template-columns:1fr; }',
          '  .evolv-asset-curve { width:100%; }',
          '}'
        ].join('\\n');
        const style = document.createElement('style');
        style.id = id;
        style.textContent = css;
        document.head.appendChild(style);
      })();

      window.EVOLV.nodes.${nodeName}.assetMenu.initVisuals = function(node) {
        const wizard = document.getElementById('evolv-asset-wizard');
        if (!wizard) return;

        const stageMap = { supplier: 'node-input-supplier', type: 'node-input-assetType', model: 'node-input-model', unit: 'node-input-unit' };
        const downstreamOf = { supplier: ['type','model','unit'], type: ['model','unit'], model: ['unit'], unit: [] };
        const getSelect = (stage) => document.getElementById(stageMap[stage]);

        const chips = Array.from(wizard.querySelectorAll('.evolv-asset-chip'));
        const combobox = document.getElementById('evolv-asset-combobox');
        const search = combobox ? combobox.querySelector('.evolv-asset-combobox-search') : null;
        const list = combobox ? combobox.querySelector('.evolv-asset-combobox-list') : null;
        const summary = document.getElementById('evolv-asset-summary');
        const specsEl = document.getElementById('evolv-asset-specs');
        const curveEl = document.getElementById('evolv-asset-curve');

        let activeStage = null;
        let activeIndex = -1;

        // Update the chip value text from the live <select>. Empty selects
        // show the placeholder; populated selects show the option label.
        function syncChip(stage) {
          const chip = chips.find((c) => c.getAttribute('data-stage') === stage);
          if (!chip) return;
          const select = getSelect(stage);
          const valueEl = chip.querySelector('.evolv-asset-chip-value');
          const labelDefault = stage === 'supplier' ? 'Select…' : '—';
          if (!select || !select.value) {
            valueEl.textContent = labelDefault;
            valueEl.setAttribute('data-empty', 'true');
            chip.disabled = false; // stage is reachable but empty
          } else {
            const opt = select.options[select.selectedIndex];
            valueEl.textContent = (opt && opt.textContent) ? opt.textContent : select.value;
            valueEl.removeAttribute('data-empty');
          }
        }

        function syncAllChips() {
          ['supplier','type','model','unit'].forEach(syncChip);
        }

        function refreshAriaSelected() {
          chips.forEach((c) => c.setAttribute('aria-selected', c.getAttribute('data-stage') === activeStage ? 'true' : 'false'));
        }

        function closeCombobox() {
          activeStage = null;
          combobox.hidden = true;
          refreshAriaSelected();
        }

        function openStage(stage) {
          const select = getSelect(stage);
          if (!select) return;

          // Skip if the parent stage hasn't been resolved (e.g. type before supplier).
          // The parent select would have an empty value in that case.
          const parentOrder = ['supplier','type','model','unit'];
          const idx = parentOrder.indexOf(stage);
          for (let i = 0; i < idx; i += 1) {
            const parentSel = getSelect(parentOrder[i]);
            if (!parentSel || !parentSel.value) {
              if (window.RED && window.RED.notify) {
                window.RED.notify('Pick ' + parentOrder[i] + ' first.', 'info');
              }
              return;
            }
          }

          activeStage = stage;
          combobox.hidden = false;
          search.value = '';
          search.placeholder = 'Filter ' + stage + '…';
          renderList('');
          refreshAriaSelected();
          // Move focus to the search box so keyboard users get an immediate
          // typing context after clicking a chip.
          setTimeout(() => search.focus(), 0);
        }

        function getStageOptions(stage) {
          const select = getSelect(stage);
          if (!select) return [];
          return Array.from(select.options)
            .filter((o) => o.value !== '' && !o.disabled)
            .map((o) => ({ value: o.value, label: o.textContent || o.value }));
        }

        function renderList(filter) {
          if (!activeStage || !list) return;
          const items = getStageOptions(activeStage);
          const lc = String(filter || '').toLowerCase();
          const matches = items.filter((it) => it.label.toLowerCase().includes(lc) || it.value.toLowerCase().includes(lc));
          list.innerHTML = '';
          activeIndex = matches.length ? 0 : -1;

          if (!matches.length) {
            const empty = document.createElement('div');
            empty.className = 'evolv-asset-combobox-empty';
            empty.textContent = items.length ? 'No matches.' : 'Nothing available — pick the previous stage first.';
            list.appendChild(empty);
            return;
          }

          matches.forEach((it, i) => {
            const opt = document.createElement('div');
            opt.className = 'evolv-asset-combobox-option';
            if (i === 0) opt.classList.add('evolv-asset-combobox-option-active');
            opt.setAttribute('role', 'option');
            opt.setAttribute('data-value', it.value);
            opt.textContent = it.label;
            opt.addEventListener('mousedown', (e) => { e.preventDefault(); pickValue(it.value); });
            opt.addEventListener('mouseenter', () => {
              activeIndex = i;
              list.querySelectorAll('.evolv-asset-combobox-option').forEach((el, j) => el.classList.toggle('evolv-asset-combobox-option-active', j === i));
            });
            list.appendChild(opt);
          });
        }

        function pickValue(value) {
          const select = getSelect(activeStage);
          if (!select) return;
          // Reset downstream selects so the cascade refreshes cleanly.
          (downstreamOf[activeStage] || []).forEach((s) => {
            const ds = getSelect(s);
            if (ds) { ds.value = ''; ds.dispatchEvent(new Event('change', { bubbles: true })); }
          });
          select.value = value;
          select.dispatchEvent(new Event('change', { bubbles: true }));
          syncAllChips();
          updateSummary();
          closeCombobox();

          // Auto-advance to the next empty stage so the flow feels guided.
          const order = ['supplier','type','model','unit'];
          const i = order.indexOf(activeStage);
          for (let n = i + 1; n < order.length; n += 1) {
            const next = getSelect(order[n]);
            if (next && (!next.value || next.options.length > 1)) {
              openStage(order[n]);
              return;
            }
          }
        }

        function updateSummary() {
          const modelSel = getSelect('model');
          if (!modelSel || !modelSel.value) {
            if (summary) summary.hidden = true;
            return;
          }
          if (summary) summary.hidden = false;

          // Lookup the chosen model in the menuData tree to pull metadata + previewCurve.
          const data = window.EVOLV.nodes.${nodeName}.menuData.asset || {};
          const categories = data.categories || {};
          let chosenModel = null;
          Object.keys(categories).forEach((catKey) => {
            const cat = categories[catKey];
            (cat.suppliers || []).forEach((sup) => (sup.types || []).forEach((t) => (t.models || []).forEach((m) => {
              if (String(m.id || m.name) === String(modelSel.value)) chosenModel = m;
            })));
          });

          renderSpecs(chosenModel);
          renderCurve(chosenModel && chosenModel.previewCurve);
        }

        function renderSpecs(model) {
          if (!specsEl) return;
          specsEl.innerHTML = '';
          if (!model) return;
          const rows = [];
          if (model.name) rows.push({ key: 'Name', val: model.name });
          if (model.id && model.id !== model.name) rows.push({ key: 'ID', val: model.id });
          if (Array.isArray(model.units) && model.units.length) rows.push({ key: 'Units', val: model.units.join(', ') });
          // Pull any leftover scalar keys (rated_kW, voltage, etc.) — heuristic.
          Object.keys(model).forEach((k) => {
            if (['name','id','units','previewCurve','product_model_id','product_model_uuid'].indexOf(k) >= 0) return;
            const v = model[k];
            if (v == null) return;
            if (typeof v === 'object') return;
            rows.push({ key: k, val: String(v) });
          });
          rows.slice(0, 5).forEach((r) => {
            const row = document.createElement('div');
            row.className = 'evolv-asset-spec-row';
            row.innerHTML = '<span class="evolv-asset-spec-key">' + r.key + '</span><span class="evolv-asset-spec-val">' + r.val + '</span>';
            specsEl.appendChild(row);
          });
        }

        function renderCurve(curve) {
          if (!curveEl) return;
          curveEl.innerHTML = '';
          if (!curve || !Array.isArray(curve.x) || !Array.isArray(curve.y) || curve.x.length < 2) {
            const empty = document.createElement('div');
            empty.className = 'evolv-asset-curve-empty';
            empty.textContent = 'no curve available';
            curveEl.appendChild(empty);
            return;
          }
          const W = 200, H = 90, P = 6;
          const xs = curve.x, ys = curve.y;
          const xMin = Math.min.apply(null, xs), xMax = Math.max.apply(null, xs);
          const yMin = Math.min.apply(null, ys), yMax = Math.max.apply(null, ys);
          const xRange = xMax - xMin || 1, yRange = yMax - yMin || 1;
          const px = (x) => P + (W - 2*P) * (x - xMin) / xRange;
          const py = (y) => (H - P) - (H - 2*P) * (y - yMin) / yRange;
          const pts = xs.map((x, i) => px(x).toFixed(1) + ',' + py(ys[i]).toFixed(1)).join(' ');
          const svg = [
            '<svg viewBox="0 0 ' + W + ' ' + H + '" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">',
            '  <rect x="0" y="0" width="' + W + '" height="' + H + '" fill="#fff" stroke="#e5e5e5"/>',
            '  <polyline fill="none" stroke="#1F4E79" stroke-width="1.6" points="' + pts + '"/>',
            '  <g font-size="8" fill="#888" font-family="Arial, sans-serif">',
            '    <text x="' + P + '" y="9">' + (curve.yLabel || '') + '</text>',
            '    <text x="' + (W - P) + '" y="' + (H - 2) + '" text-anchor="end">' + (curve.xLabel || '') + '</text>',
            (curve.legend ? '<text x="' + (W - P) + '" y="9" text-anchor="end" fill="#1F4E79">' + curve.legend + '</text>' : ''),
            '  </g>',
            '</svg>'
          ].join('');
          curveEl.innerHTML = svg;
        }

        // --- Wire chip clicks + select-change → chip refresh -------------
        chips.forEach((chip) => {
          chip.addEventListener('click', () => {
            const stage = chip.getAttribute('data-stage');
            if (activeStage === stage) {
              closeCombobox();
            } else {
              openStage(stage);
            }
          });
        });
        ['supplier','type','model','unit'].forEach((stage) => {
          const sel = getSelect(stage);
          if (sel) sel.addEventListener('change', () => { syncChip(stage); if (stage === 'model' || stage === 'unit') updateSummary(); });
        });

        // --- Combobox interactions -------------------------------------
        if (search) {
          search.addEventListener('input', () => renderList(search.value));
          search.addEventListener('keydown', (e) => {
            const optEls = Array.from(list.querySelectorAll('.evolv-asset-combobox-option'));
            if (!optEls.length) return;
            if (e.key === 'ArrowDown') {
              e.preventDefault();
              activeIndex = (activeIndex + 1) % optEls.length;
              optEls.forEach((el, i) => el.classList.toggle('evolv-asset-combobox-option-active', i === activeIndex));
              optEls[activeIndex].scrollIntoView({ block: 'nearest' });
            } else if (e.key === 'ArrowUp') {
              e.preventDefault();
              activeIndex = (activeIndex - 1 + optEls.length) % optEls.length;
              optEls.forEach((el, i) => el.classList.toggle('evolv-asset-combobox-option-active', i === activeIndex));
              optEls[activeIndex].scrollIntoView({ block: 'nearest' });
            } else if (e.key === 'Enter') {
              e.preventDefault();
              if (activeIndex >= 0 && optEls[activeIndex]) {
                pickValue(optEls[activeIndex].getAttribute('data-value'));
              }
            } else if (e.key === 'Escape') {
              e.preventDefault();
              closeCombobox();
            }
          });
        }

        // Initial render — fires after loadData has populated the natives.
        syncAllChips();
        updateSummary();
      };
    `;
  }

  getClientInitCode(nodeName) {
    const htmlCode = this.getHtmlInjectionCode(nodeName);
    const dataCode = this.getDataInjectionCode(nodeName);
    const eventsCode = this.getEventInjectionCode(nodeName);
    const syncCode = this.getSyncInjectionCode(nodeName);
    const saveCode = this.getSaveInjectionCode(nodeName);
    const visualCode = this.getVisualInjectionCode(nodeName);

    return `
      // --- AssetMenu for ${nodeName} ---
      window.EVOLV.nodes.${nodeName}.assetMenu =
        window.EVOLV.nodes.${nodeName}.assetMenu || {};

      ${htmlCode}
      ${dataCode}
      ${eventsCode}
      ${syncCode}
      ${saveCode}
      ${visualCode}

      window.EVOLV.nodes.${nodeName}.assetMenu.initEditor = function(node) {
        console.log('Initializing asset properties for ${nodeName}');
        this.injectHtml();
        this.wireEvents(node);
        const self = this;
        this.loadData(node)
          .then(() => { if (self.initVisuals) self.initVisuals(node); })
          .catch((error) => {
            console.error('Asset menu load failed:', error);
            if (self.initVisuals) self.initVisuals(node);
          });
      };
    `;
  }

  getDataInjectionCode(nodeName) {
    return `
      // Asset data loader for ${nodeName}
      window.EVOLV.nodes.${nodeName}.assetMenu.loadData = async function(node) {
        const menuAsset = window.EVOLV.nodes.${nodeName}.menuData.asset || {};
        const categories = menuAsset.categories || {};
        const defaultCategory = menuAsset.defaultCategory || Object.keys(categories)[0] || null;
        const apiConfig = menuAsset.apiConfig || {};
        const elems = {
          supplier: document.getElementById('node-input-supplier'),
          type: document.getElementById('node-input-assetType'),
          model: document.getElementById('node-input-model'),
          unit: document.getElementById('node-input-unit')
        };

        function resolveCategoryKey() {
          if (node.softwareType && categories[node.softwareType]) {
            return node.softwareType;
          }
          if (node.category && categories[node.category]) {
            return node.category;
          }
          return defaultCategory;
        }

        function normalizeModel(model = {}) {
          return {
            id: model.id ?? model.name,
            name: model.name,
            units: model.units || []
          };
        }

        function normalizeType(type = {}) {
          return {
            id: type.id || type.name,
            name: type.name,
            models: Array.isArray(type.models)
              ? type.models.map(normalizeModel)
              : []
          };
        }

        function normalizeSupplier(supplier = {}) {
          const types = (supplier.categories || []).reduce((acc, category) => {
            const categoryTypes = Array.isArray(category.types)
              ? category.types.map(normalizeType)
              : [];
            return acc.concat(categoryTypes);
          }, []);
          return {
            id: supplier.id || supplier.name,
            name: supplier.name,
            types
          };
        }

        function normalizeApiCategory(key, label, suppliers = []) {
          const normalizedSuppliers = suppliers
            .map(normalizeSupplier)
            .filter((supplier) => supplier.types && supplier.types.length);
          if (!normalizedSuppliers.length) {
            return null;
          }
          return {
            softwareType: key,
            label: label || key,
            suppliers: normalizedSuppliers
          };
        }

        async function fetchCategoryFromApi(key) {
          if (!apiConfig.url || !key) {
            return null;
          }
          const response = await fetch(apiConfig.url, {
            headers: apiConfig.headers || {}
          });
          if (!response.ok) {
            throw new Error('Asset API request failed: ' + response.status);
          }
          const payload = await response.json();
          if (!payload || payload.success === false || !Array.isArray(payload.data)) {
            throw new Error(payload?.message || 'Unexpected asset API response');
          }
          return normalizeApiCategory(key, node.softwareType || key, payload.data);
        }

        // Non-dispatching populate (matches the wireEvents version). The
        // load path below explicitly walks supplier -> type -> model ->
        // unit in order using saved node.* values, so auto-dispatched
        // change events (which previously cascaded through wireEvents'
        // listeners and double-populated everything) are no longer needed.
        function populate(selectEl, items = [], selectedValue, mapFn, placeholderText = 'Select...') {
          if (!selectEl) return;
          const mapper = typeof mapFn === 'function'
            ? mapFn
            : (value) => ({ value, label: value });

          selectEl.innerHTML = '';

          const placeholder = document.createElement('option');
          placeholder.value = '';
          placeholder.textContent = placeholderText;
          placeholder.disabled = true;
          placeholder.selected = true;
          selectEl.appendChild(placeholder);

          items.forEach((item) => {
            const option = mapper(item);
            if (!option || typeof option.value === 'undefined') {
              return;
            }
            const opt = document.createElement('option');
            opt.value = option.value;
            opt.textContent = option.label;
            selectEl.appendChild(opt);
          });

          if (selectedValue) {
            selectEl.value = selectedValue;
            if (!selectEl.value) {
              selectEl.value = '';
            }
          } else {
            selectEl.value = '';
          }
        }

        const categoryKey = resolveCategoryKey();
        const resolvedCategoryKey = categoryKey || defaultCategory;
        let activeCategory = resolvedCategoryKey ? categories[resolvedCategoryKey] : null;

        if (resolvedCategoryKey) {
          node.category = resolvedCategoryKey;
        }

        try {
          const apiCategory = await fetchCategoryFromApi(resolvedCategoryKey);
          if (apiCategory) {
            categories[resolvedCategoryKey] = apiCategory;
            activeCategory = apiCategory;
          }
        } catch (error) {
          console.warn('[AssetMenu] API lookup failed for ${nodeName}, using local asset data', error);
        }

        const suppliers = activeCategory ? activeCategory.suppliers : [];

        // The save handler intentionally discards node.supplier / node.assetType
        // (denormalized copies of registry data — only node.model + node.unit
        // are persisted identity). So on reopen we re-derive them from the
        // saved model id by walking the registry tree. Without this the
        // cascade always boots at "Select..." even when a model is saved.
        if (node.model && (!node.supplier || !node.assetType)) {
          for (const supplier of suppliers) {
            const match = (supplier.types || []).find((type) =>
              (type.models || []).some((model) =>
                String(model.id || model.name) === String(node.model))
            );
            if (match) {
              node.supplier = supplier.id || supplier.name;
              node.assetType = match.id || match.name;
              break;
            }
          }
        }

        populate(
          elems.supplier,
          suppliers,
          node.supplier,
          (supplier) => ({ value: supplier.id || supplier.name, label: supplier.name }),
          suppliers.length ? 'Select...' : 'No suppliers available'
        );

        const activeSupplier = suppliers.find(
          (supplier) => String(supplier.id || supplier.name) === String(node.supplier)
        );
        const types = activeSupplier ? activeSupplier.types : [];
        populate(
          elems.type,
          types,
          node.assetType,
          (type) => ({ value: type.id || type.name, label: type.name }),
          activeSupplier ? 'Select...' : 'Awaiting Supplier Selection'
        );

        const activeType = types.find(
          (type) => String(type.id || type.name) === String(node.assetType)
        );
        const models = activeType ? activeType.models : [];
        populate(
          elems.model,
          models,
          node.model,
          (model) => ({ value: model.id || model.name, label: model.name }),
          activeType ? 'Select...' : 'Awaiting Type Selection'
        );

        const activeModel = models.find(
          (model) => String(model.id || model.name) === String(node.model)
        );
        if (activeModel) {
          node.modelMetadata = activeModel;
          node.modelName = activeModel.name;
        }
        populate(
          elems.unit,
          activeModel ? activeModel.units || [] : [],
          node.unit,
          (unit) => ({ value: unit, label: unit }),
          activeModel ? 'Select...' : activeType ? 'Awaiting Model Selection' : 'Awaiting Type Selection'
        );
        this.setAssetTagNumber(node, node.assetTagNumber || '');
      };
    `;
  }

  getEventInjectionCode(nodeName) {
    return `
      // Asset event wiring for ${nodeName}
      //
      // The supplier -> type -> model -> unit chain is a strict downward
      // cascade: each select rebuilds the next based on the currently
      // selected value above it. Two earlier bugs in this code:
      //
      //   1. populate() auto-dispatched a synthetic 'change' event whenever
      //      the value of the rebuilt select differed from before the
      //      rebuild. That triggered the *child* select's listener mid-way
      //      through the *parent* listener, which then continued and
      //      blindly overwrote the child select with empty content. Net
      //      effect: model dropdown showed 'Awaiting Type Selection' even
      //      though a type was clearly selected.
      //
      //   2. Each downstream wipe ran unconditionally inside the parent
      //      handler, instead of being driven by the actual current value
      //      of the child select.
      //
      // Fix: populate() no longer dispatches change. Cascade is explicit
      // via cascadeFromSupplier() / cascadeFromType() / cascadeFromModel()
      // which are called from each handler. The same helpers run on
      // initial load so behaviour is identical whether the user picked the
      // value or it came from a saved node.
      window.EVOLV.nodes.${nodeName}.assetMenu.wireEvents = function(node) {
        const menuAsset = window.EVOLV.nodes.${nodeName}.menuData.asset || {};
        const categories = menuAsset.categories || {};
        const defaultCategory = menuAsset.defaultCategory || Object.keys(categories)[0] || null;
        const elems = {
          supplier: document.getElementById('node-input-supplier'),
          type: document.getElementById('node-input-assetType'),
          model: document.getElementById('node-input-model'),
          unit: document.getElementById('node-input-unit')
        };

        // populate(): rebuild a <select> with a placeholder + items.
        // No change-event dispatch — cascading is done explicitly by the
        // caller via cascadeFrom*() so the order of operations is
        // predictable.
        function populate(selectEl, items, selectedValue, mapFn, placeholderText) {
          if (!selectEl) return;
          if (!Array.isArray(items)) items = [];
          if (!placeholderText) placeholderText = 'Select...';
          const mapper = typeof mapFn === 'function'
            ? mapFn
            : (value) => ({ value: value, label: value });

          selectEl.innerHTML = '';

          const placeholder = document.createElement('option');
          placeholder.value = '';
          placeholder.textContent = placeholderText;
          placeholder.disabled = true;
          placeholder.selected = true;
          selectEl.appendChild(placeholder);

          items.forEach(function (item) {
            const option = mapper(item);
            if (!option || typeof option.value === 'undefined') return;
            const opt = document.createElement('option');
            opt.value = option.value;
            opt.textContent = option.label;
            selectEl.appendChild(opt);
          });

          if (selectedValue) {
            selectEl.value = selectedValue;
            if (!selectEl.value) selectEl.value = '';
          } else {
            selectEl.value = '';
          }
        }

        function resolveCategoryKey() {
          if (node.softwareType && categories[node.softwareType]) return node.softwareType;
          if (node.category && categories[node.category]) return node.category;
          return defaultCategory;
        }
        function getActiveCategory() {
          const key = resolveCategoryKey();
          return key ? categories[key] : null;
        }

        node.category = resolveCategoryKey();

        // Lookup helpers — read from the *currently selected* values in the
        // DOM, not from node.* (which may not yet be in sync).
        function findSupplier() {
          const cat = getActiveCategory();
          if (!cat || !Array.isArray(cat.suppliers)) return null;
          const id = String(elems.supplier.value);
          return cat.suppliers.find(function (s) {
            return String(s.id || s.name) === id;
          }) || null;
        }
        function findType(supplier) {
          if (!supplier || !Array.isArray(supplier.types)) return null;
          const id = String(elems.type.value);
          return supplier.types.find(function (t) {
            return String(t.id || t.name) === id;
          }) || null;
        }
        function findModel(type) {
          if (!type || !Array.isArray(type.models)) return null;
          const id = String(elems.model.value);
          return type.models.find(function (m) {
            return String(m.id || m.name) === id;
          }) || null;
        }

        // === Cascade rebuild functions ==========================
        // Each one rebuilds the dropdown for the *level it owns* plus all
        // levels below it, using the current values in the DOM. Called by
        // the corresponding change handler AND by initial load so both
        // paths produce identical state.

        function cascadeFromSupplier() {
          const supplier = findSupplier();
          const types = supplier ? supplier.types : [];
          populate(
            elems.type,
            types,
            node.assetType,
            function (t) { return { value: t.id || t.name, label: t.name }; },
            supplier ? 'Select...' : 'Awaiting Supplier Selection'
          );
          // After repopulating type, propagate down. cascadeFromType()
          // will read the new elems.type.value (which was set by populate
          // to either the saved node.assetType or '') and rebuild model.
          cascadeFromType();
        }

        function cascadeFromType() {
          const supplier = findSupplier();
          const type = findType(supplier);
          const models = type ? type.models : [];
          populate(
            elems.model,
            models,
            node.model,
            function (m) { return { value: m.id || m.name, label: m.name }; },
            type ? 'Select...' : 'Awaiting Type Selection'
          );
          node.modelMetadata = null;
          cascadeFromModel();
        }

        function cascadeFromModel() {
          const supplier = findSupplier();
          const type = findType(supplier);
          const model = findModel(type);
          node.modelMetadata = model;
          node.modelName = model ? model.name : '';
          populate(
            elems.unit,
            model ? (model.units || []) : [],
            node.unit,
            function (u) { return { value: u, label: u }; },
            model ? 'Select...' : (type ? 'Awaiting Model Selection' : 'Awaiting Type Selection')
          );
        }

        elems.supplier.addEventListener('change', cascadeFromSupplier);
        elems.type.addEventListener('change', cascadeFromType);
        elems.model.addEventListener('change', cascadeFromModel);

        // Expose the cascades so loadData() (or future code) can re-run
        // them after async data arrives without duplicating logic.
        window.EVOLV.nodes.${nodeName}.assetMenu._cascade = {
          fromSupplier: cascadeFromSupplier,
          fromType: cascadeFromType,
          fromModel: cascadeFromModel,
        };
      };
    `;
  }

  getSyncInjectionCode(nodeName) {
    return `
      // Asset synchronization helpers for ${nodeName}
      window.EVOLV.nodes.${nodeName}.assetMenu.setAssetTagNumber = function(node, tag) {
        const normalized = tag ? tag.toString() : '';
        const input = document.getElementById('node-input-assetTagNumber');
        const hint = document.getElementById('node-input-assetTagNumber-hint');
        console.info('[AssetMenu] tag number update', {
          nodeId: node && node.id ? node.id : null,
          tag: normalized
        });
        if (input) {
          input.value = normalized;
        }
        if (hint) {
          hint.textContent = normalized ? 'Assigned tag ' + normalized : 'Not registered yet';
        }
        if (node) {
          node.assetTagNumber = normalized;
        }
      };

      window.EVOLV.nodes.${nodeName}.assetMenu.buildSyncRequest = function(node) {
        const tagInput = document.getElementById('node-input-assetTagNumber');
        const candidateTag = tagInput && tagInput.value ? tagInput.value.trim() : '';
        const fallbackTag = node && node.assetTagNumber ? node.assetTagNumber : '';
        const registrationDefaults =
          (window.EVOLV.nodes.${nodeName}.config && window.EVOLV.nodes.${nodeName}.config.assetRegistration && window.EVOLV.nodes.${nodeName}.config.assetRegistration.default) || {};
        const displayName = node && node.name ? node.name : node && node.id ? node.id : '${nodeName}';
        console.info('[AssetMenu] build sync payload', {
          nodeId: node && node.id ? node.id : null,
          candidateTag,
          fallbackTag,
          status: registrationDefaults.status || 'actief'
        });
        return {
          asset: {
            tagNumber: candidateTag || fallbackTag,
            supplier: node && node.supplier ? node.supplier : '',
            assetType: node && node.assetType ? node.assetType : '',
            model: node && node.model ? node.model : '',
            unit: node && node.unit ? node.unit : '',
            assetName: displayName,
            assetDescription: displayName,
            assetStatus: registrationDefaults.status || 'actief',
            modelMetadata: node && node.modelMetadata ? node.modelMetadata : null
          },
          nodeId: node && node.id ? node.id : null,
          nodeName: node && node.type ? node.type : '${nodeName}'
        };
      };

      window.EVOLV.nodes.${nodeName}.assetMenu.syncAsset = function(node) {
        const payload = this.buildSyncRequest(node);
        const redSettings = window.RED && window.RED.settings;
        const adminRoot = redSettings ? redSettings.httpAdminRoot : '';
        const trimmedRoot = adminRoot && adminRoot.endsWith('/') ? adminRoot.slice(0, -1) : adminRoot || '';
        const prefix = trimmedRoot || '';
        const endpoint = (prefix || '') + '/${nodeName}/asset-reg';
        console.info('[AssetMenu] sync request', {
          endpoint,
          nodeId: node && node.id ? node.id : null,
          tagNumber: payload && payload.asset ? payload.asset.tagNumber : null
        });
        fetch(endpoint, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload)
        })
          .then((res) =>
            res.json().catch((err) => {
              console.warn('[AssetMenu] asset sync response is not JSON', err);
              return { success: false, message: err.message || 'Invalid API response' };
            })
          )
          .then((result) => {
            console.info('[AssetMenu] sync response', result);
            if (result && result.success) {
              const newTag = (result.data && result.data.asset_tag_number) || payload.asset.tagNumber || '';
              this.setAssetTagNumber(node, newTag);
              if (window.RED && typeof window.RED.notify === 'function') {
                window.RED.notify('Asset synced: ' + (newTag || 'no tag'), 'info');
              }
            } else {
              console.warn('[AssetMenu] asset sync failed', result && result.message);
            }
          })
          .catch((error) => {
            console.error('[AssetMenu] asset sync error', error);
          });
      };
    `;
  }

  getHtmlTemplate() {
    // Wizard layout:
    //   1. Section heading + chip strip (Supplier › Type › Model › Unit).
    //      Chips are clickable buttons; clicking re-opens that stage's combobox
    //      and resets everything to its right.
    //   2. Active-stage combobox: search input + filtered option list.
    //   3. Spec strip + curve mini-chart (visible once a Model is picked).
    //   4. Asset Tag row (still read-only, auto-resolved by syncAsset).
    //   5. Hidden native <select>s (canonical save targets — Node-RED reads
    //      these on save; chip clicks mirror values into them).
    return `
      <hr />
      <h3>Asset selection</h3>
      <div class="evolv-asset-wizard" id="evolv-asset-wizard">
        <div class="evolv-asset-chips" role="tablist" aria-label="Asset selection stages">
          <button type="button" class="evolv-asset-chip" data-stage="supplier" aria-selected="false">
            <span class="evolv-asset-chip-icon"><i class="fa fa-industry"></i></span>
            <span class="evolv-asset-chip-text">
              <span class="evolv-asset-chip-label">Supplier</span>
              <span class="evolv-asset-chip-value" data-empty="true">Select…</span>
            </span>
          </button>
          <span class="evolv-asset-chip-sep" aria-hidden="true">›</span>
          <button type="button" class="evolv-asset-chip" data-stage="type" aria-selected="false">
            <span class="evolv-asset-chip-icon"><i class="fa fa-puzzle-piece"></i></span>
            <span class="evolv-asset-chip-text">
              <span class="evolv-asset-chip-label">Type</span>
              <span class="evolv-asset-chip-value" data-empty="true">—</span>
            </span>
          </button>
          <span class="evolv-asset-chip-sep" aria-hidden="true">›</span>
          <button type="button" class="evolv-asset-chip" data-stage="model" aria-selected="false">
            <span class="evolv-asset-chip-icon"><i class="fa fa-wrench"></i></span>
            <span class="evolv-asset-chip-text">
              <span class="evolv-asset-chip-label">Model</span>
              <span class="evolv-asset-chip-value" data-empty="true">—</span>
            </span>
          </button>
          <span class="evolv-asset-chip-sep" aria-hidden="true">›</span>
          <button type="button" class="evolv-asset-chip" data-stage="unit" aria-selected="false">
            <span class="evolv-asset-chip-icon"><i class="fa fa-balance-scale"></i></span>
            <span class="evolv-asset-chip-text">
              <span class="evolv-asset-chip-label">Unit</span>
              <span class="evolv-asset-chip-value" data-empty="true">—</span>
            </span>
          </button>
        </div>

        <div class="evolv-asset-combobox" id="evolv-asset-combobox" hidden>
          <input type="text" class="evolv-asset-combobox-search" placeholder="Type to filter…" autocomplete="off" />
          <div class="evolv-asset-combobox-list" role="listbox"></div>
        </div>

        <div class="evolv-asset-summary" id="evolv-asset-summary" hidden>
          <div class="evolv-asset-specs" id="evolv-asset-specs"></div>
          <div class="evolv-asset-curve" id="evolv-asset-curve"></div>
        </div>

        <div class="form-row evolv-asset-tag-row">
          <label for="node-input-assetTagNumber"><i class="fa fa-hashtag"></i> Asset Tag</label>
          <input type="text" id="node-input-assetTagNumber" readonly />
          <div class="form-tips" id="node-input-assetTagNumber-hint">Not registered yet</div>
        </div>

        <div class="evolv-asset-hidden-natives" aria-hidden="true">
          <select id="node-input-supplier"></select>
          <select id="node-input-assetType"></select>
          <select id="node-input-model"></select>
          <select id="node-input-unit"></select>
        </div>
      </div>
      <hr />
    `;
  }

  // Build a slim preview curve `{x[], y[], xLabel, yLabel}` per model so the
  // editor wizard can render a sparkline without round-tripping. Picks a
  // representative slice for each software type's curve format.
  buildPreviewCurve(softwareType, modelId, modelName) {
    if (!modelId && !modelName) return null;
    let loadCurve;
    try {
      // Lazy require — keep AssetMenu importable in environments that don't
      // ship the curves dataset (e.g. unit tests with mocked managers).
      loadCurve = require('../../index.js').loadCurve;
    } catch (e) {
      return null;
    }
    if (typeof loadCurve !== 'function') return null;

    // Try id first, then name (legacy curve files are named after the
    // model name rather than id — e.g. ECDV.json).
    let curve = null;
    try { curve = loadCurve(modelId) || (modelName ? loadCurve(modelName) : null); } catch (e) { curve = null; }
    if (!curve) return null;

    const type = String(softwareType || '').toLowerCase();

    // Helpers — pick a "middle" key from an object whose keys are numeric strings.
    const middleKey = (obj) => {
      const keys = Object.keys(obj || {});
      if (!keys.length) return null;
      const sorted = keys.slice().sort((a, b) => Number(a) - Number(b));
      return sorted[Math.floor(sorted.length / 2)];
    };
    const maxKey = (obj) => {
      const keys = Object.keys(obj || {});
      if (!keys.length) return null;
      return keys.slice().sort((a, b) => Number(b) - Number(a))[0];
    };

    try {
      if (type === 'rotatingmachine') {
        // { np: { rpm: { x:[%speed], y:[..] } } } — pick top RPM slice.
        const np = curve.np || curve;
        const rpm = maxKey(np);
        if (!rpm || !np[rpm] || !Array.isArray(np[rpm].x)) return null;
        return {
          x: np[rpm].x.slice(),
          y: np[rpm].y.slice(),
          xLabel: 'Speed (%)',
          yLabel: 'Power',
          legend: rpm + ' rpm'
        };
      }
      if (type === 'valve') {
        // { density: { dp: { x:[%opening], y:[m3/h] } } } — pick mid density/dp.
        const densityKey = middleKey(curve);
        if (!densityKey) return null;
        const dpMap = curve[densityKey] || {};
        const dpKey = middleKey(dpMap);
        if (!dpKey || !dpMap[dpKey] || !Array.isArray(dpMap[dpKey].x)) return null;
        return {
          x: dpMap[dpKey].x.slice(),
          y: dpMap[dpKey].y.slice(),
          xLabel: 'Opening (%)',
          yLabel: 'Flow (m³/h)',
          legend: 'ρ=' + densityKey + ' · Δp=' + dpKey
        };
      }
      if (type === 'diffuser') {
        // { sote_curve: { coverage: { x:[flux], y:[%] } }, ... } — pick mid coverage on sote_curve.
        const sote = curve.sote_curve || curve.SOTE_curve || curve;
        const covKey = middleKey(sote);
        if (!covKey || !sote[covKey] || !Array.isArray(sote[covKey].x)) return null;
        return {
          x: sote[covKey].x.slice(),
          y: sote[covKey].y.slice(),
          xLabel: 'Flux (Nm³/h·m²)',
          yLabel: 'SOTE (%)',
          legend: covKey + '% coverage'
        };
      }
      // measurement + unknowns: no representative curve yet.
      return null;
    } catch (e) {
      return null;
    }
  }

  getHtmlInjectionCode(nodeName) {
    const htmlTemplate = this.getHtmlTemplate()
      .replace(/`/g, '\\`')
      .replace(/\$/g, '\\$');

    return `
      // Asset HTML injection for ${nodeName}
      window.EVOLV.nodes.${nodeName}.assetMenu.injectHtml = function() {
        const placeholder = document.getElementById('asset-fields-placeholder');
        if (placeholder && !placeholder.hasChildNodes()) {
          placeholder.innerHTML = \`${htmlTemplate}\`;
          console.log('Asset HTML injected successfully');
        }
      };
    `;
  }

  getSaveInjectionCode(nodeName) {
    // After the AssetResolver cutover, only model + unit + tagCode are stored
    // on the node. supplier / assetType / category were denormalized copies of
    // registry data and are derived at runtime via
    // assetResolver.resolveAssetMetadata(softwareType, model).
    //
    // We still READ the supplier/type DOM elements for validation (the user
    // must have walked the cascade to pick a model), but we explicitly CLEAR
    // them from the persisted node — so a saved flow only contains the
    // identifier surface.
    return `
      // Asset save handler for ${nodeName}
      window.EVOLV.nodes.${nodeName}.assetMenu.saveEditor = function(node) {
        console.log('Saving asset properties for ${nodeName}');
        const errors = [];

        const modelEl = document.getElementById('node-input-model');
        const unitEl  = document.getElementById('node-input-unit');
        const tagEl   = document.getElementById('node-input-assetTagNumber');

        node.model           = modelEl ? modelEl.value : '';
        node.unit            = unitEl  ? unitEl.value  : '';
        node.assetTagNumber  = tagEl   ? tagEl.value   : '';

        // Identity surface only — registry derives the rest.
        delete node.supplier;
        delete node.category;
        delete node.assetType;

        if (!node.model) errors.push('Model is required.');
        if (!node.unit)  errors.push('Unit is required.');

        errors.forEach((msg) => RED.notify(msg, 'error'));

        const saved = { model: node.model, unit: node.unit, assetTagNumber: node.assetTagNumber };
        if (node.modelMetadata && typeof node.modelMetadata.id !== 'undefined') {
          saved.modelId = node.modelMetadata.id;
        }
        console.log('[AssetMenu] save result:', saved);

        if (errors.length === 0 && this.syncAsset) {
          this.syncAsset(node);
        }

        return errors.length === 0;
      };
    `;
  }
}

module.exports = AssetMenu;
