/**
 * Страница Helm-аддонов: GET /api/v1/clusters, статус и POST/DELETE …/addons/*.
 *
 * Автор: Сергей Антропов
 * Сайт: https://devops.org.ru
 */
(function () {
  "use strict";

  const body = document.body;
  /* trim: лишний пробел/перенос в шаблоне ломал бы проверку — скрипт выходил бы до bootstrap и оверлей не снимался */
  var dashMode = (body.getAttribute("data-dashboard-mode") || body.dataset.dashboardMode || "").trim();
  if (dashMode !== "addons") return;

  const API = (body.dataset.apiBase || "/api/v1").replace(/\/$/, "");

  /** data-endpoint кнопки → поле ответа GET …/addons/status */
  var ENDPOINT_TO_STATUS_KEY = {
    "ingress-nginx": "ingress_nginx",
    "kube-prometheus-stack": "kube_prometheus_stack",
    "metrics-server": "metrics_server",
    "istio-kiali": "istio_mesh_ready",
  };

  var ADDON_HUMAN_LABEL = {
    "ingress-nginx": "ingress-nginx",
    "kube-prometheus-stack": "kube-prometheus-stack",
    "metrics-server": "metrics-server",
    "istio-kiali": "Istio + Kiali",
  };

  /** Последний успешный снимок статуса (для подписи «Установить» / «Обновить»). */
  var lastAddonsStatus = null;

  /** Список версий из GET /helm/chart-versions уже загружен — можно мержить версии из кластера. */
  var chartVersionsReady = false;

  /** Суффикс подписи для версии, совпадающей с установленной в кластере. */
  var INSTALLED_VER_SUFFIX = " (текущая установленная версия)";

  /** @type {((value: boolean) => void) | null} */
  var confirmModalResolver = null;
  var confirmModalIsAlert = false;

  function isConfirmModalOpen() {
    const el = document.getElementById("confirm-modal-overlay");
    return !!(el && !el.classList.contains("hidden"));
  }

  function closeConfirmModal(confirmed) {
    const ov = document.getElementById("confirm-modal-overlay");
    const cancelBtn = document.getElementById("confirm-modal-cancel");
    if (ov) ov.classList.add("hidden");
    document.body.classList.remove("modal-open");
    if (cancelBtn) cancelBtn.classList.remove("hidden");
    if (confirmModalResolver) {
      var fn = confirmModalResolver;
      confirmModalResolver = null;
      var out = confirmModalIsAlert ? true : !!confirmed;
      confirmModalIsAlert = false;
      fn(out);
    } else {
      confirmModalIsAlert = false;
    }
  }

  /**
   * Модальное подтверждение (разметка из partials/dashboard_modals.html).
   * @param {{ title?: string, message: string, confirmLabel?: string, danger?: boolean, alert?: boolean }} opts
   * @returns {Promise<boolean>}
   */
  function openConfirmModal(opts) {
    return new Promise(function (resolve) {
      const ov = document.getElementById("confirm-modal-overlay");
      const titleEl = document.getElementById("confirm-modal-title");
      const msgEl = document.getElementById("confirm-modal-message");
      const okBtn = document.getElementById("confirm-modal-ok");
      const cancelBtn = document.getElementById("confirm-modal-cancel");
      if (!ov || !titleEl || !msgEl || !okBtn) {
        resolve(false);
        return;
      }
      if (confirmModalResolver) {
        const prevFn = confirmModalResolver;
        confirmModalResolver = null;
        confirmModalIsAlert = false;
        prevFn(false);
        ov.classList.add("hidden");
        document.body.classList.remove("modal-open");
        if (cancelBtn) cancelBtn.classList.remove("hidden");
      }
      confirmModalResolver = resolve;
      confirmModalIsAlert = !!opts.alert;
      titleEl.textContent = opts.title || "Подтвердите действие";
      msgEl.textContent = opts.message || "";
      okBtn.textContent = opts.confirmLabel || (confirmModalIsAlert ? "Понятно" : "Подтвердить");
      okBtn.className = opts.danger ? "btn-danger" : "";
      if (cancelBtn) {
        cancelBtn.classList.toggle("hidden", confirmModalIsAlert);
      }
      ov.classList.remove("hidden");
      document.body.classList.add("modal-open");
      okBtn.focus();
    });
  }

  function bindConfirmModalControls() {
    const confirmOv = document.getElementById("confirm-modal-overlay");
    const confirmCancel = document.getElementById("confirm-modal-cancel");
    const confirmOk = document.getElementById("confirm-modal-ok");
    if (confirmCancel) {
      confirmCancel.addEventListener("click", function () {
        closeConfirmModal(false);
      });
    }
    if (confirmOk) {
      confirmOk.addEventListener("click", function () {
        closeConfirmModal(true);
      });
    }
    if (confirmOv) {
      confirmOv.addEventListener("click", function (ev) {
        if (ev.target === confirmOv) closeConfirmModal(confirmModalIsAlert ? true : false);
      });
    }
    document.addEventListener("keydown", function (ev) {
      if (ev.key !== "Escape" || !isConfirmModalOpen()) return;
      closeConfirmModal(false);
    });
  }

  /**
   * @param {string} path
   * @param {RequestInit} [opts]
   */
  async function api(path, opts) {
    const url = path.startsWith("http") ? path : API + path;
    const r = await fetch(url, opts);
    const text = await r.text();
    var data;
    try {
      data = text ? JSON.parse(text) : null;
    } catch (e) {
      data = { raw: text };
    }
    if (!r.ok) {
      const msg =
        typeof data.detail === "string"
          ? data.detail
          : Array.isArray(data.detail)
            ? data.detail.map(function (x) { return x.msg || JSON.stringify(x); }).join("; ")
            : text || r.statusText;
      const err = new Error(msg);
      err.status = r.status;
      throw err;
    }
    return data;
  }

  /**
   * Обёртка над Promise: если запрос «висит» (сеть, прокси), await иначе не завершится и finally не выполнится.
   * @param {Promise<unknown>} promise
   * @param {number} ms
   * @param {string} timeoutMessage
   */
  function promiseWithTimeout(promise, ms, timeoutMessage) {
    return new Promise(function (resolve, reject) {
      var finished = false;
      var to = setTimeout(function () {
        if (finished) return;
        finished = true;
        reject(new Error(timeoutMessage));
      }, ms);
      Promise.resolve(promise).then(
        function (v) {
          if (finished) return;
          finished = true;
          clearTimeout(to);
          resolve(v);
        },
        function (err) {
          if (finished) return;
          finished = true;
          clearTimeout(to);
          reject(err);
        },
      );
    });
  }

  const sel = document.getElementById("addons-cluster-select");
  const statusBar = document.getElementById("addons-status-bar");
  const addonsCardsWrap = document.getElementById("addons-cards-wrap");
  const helmWrap = document.getElementById("addons-helm-progress-wrap");
  const helmHint = document.getElementById("addons-helm-progress-hint");
  const helmBar = document.getElementById("addons-helm-progress-bar");
  const helmTrack = document.getElementById("addons-helm-progress-track");
  const helmLabel = document.getElementById("addons-helm-progress-label");
  const helmLogPanel = document.getElementById("addons-helm-log-panel");
  const bootstrapMsg = document.getElementById("addons-bootstrap-msg");

  function getClusterName() {
    if (!sel || !sel.value) return "";
    return String(sel.value).trim();
  }

  /** Карточки аддонов — только при выбранном кластере в селекте. */
  function updateAddonsCardsVisibility() {
    if (!addonsCardsWrap) return;
    var n = getClusterName();
    addonsCardsWrap.classList.toggle("hidden", !n);
    addonsCardsWrap.setAttribute("aria-hidden", n ? "false" : "true");
  }

  /**
   * Предупреждения загрузки списка кластеров / версий чартов (не путать с журналом Helm).
   * @param {string} text пустая строка — скрыть блок
   */
  function setBootstrapMsg(text) {
    if (!bootstrapMsg) return;
    var s = (text || "").trim();
    if (!s) {
      bootstrapMsg.classList.add("hidden");
      bootstrapMsg.textContent = "";
      return;
    }
    bootstrapMsg.classList.remove("hidden");
    bootstrapMsg.textContent = s;
  }

  /** Известные data-addon карточек (только они — безопасный перенос DOM без произвольного селектора). */
  var ADDON_CARD_MOUNT_KEYS = {
    "ingress-nginx": true,
    "kube-prometheus-stack": true,
    "metrics-server": true,
    "istio-kiali": true,
  };

  /**
   * Переносит блок «Ход операции Helm» в конец карточки выбранного аддона (под кнопки).
   * Если карточку не нашли — оставляем/возвращаем в #addons-helm-progress-dock.
   * @param {string} endpoint то же, что data-endpoint / data-addon
   */
  function mountHelmProgressUnderAddon(endpoint) {
    if (!helmWrap) return;
    var dock = document.getElementById("addons-helm-progress-dock");
    var ep = typeof endpoint === "string" ? endpoint.trim() : "";
    var card = null;
    if (ep && ADDON_CARD_MOUNT_KEYS[ep]) {
      card = document.querySelector('.cluster-addon-card[data-addon="' + ep + '"]');
    }
    var parent = card || dock;
    if (!parent) return;
    parent.appendChild(helmWrap);
  }

  /**
   * Показать блок «Ход операции Helm»; при переданном endpoint — сразу под соответствующей карточкой.
   * @param {string} [endpoint]
   */
  function revealHelmProgressSection(endpoint) {
    if (endpoint) {
      mountHelmProgressUnderAddon(endpoint);
    }
    if (!helmWrap) return;
    helmWrap.classList.remove("hidden");
    helmWrap.setAttribute("aria-hidden", "false");
    requestAnimationFrame(function () {
      try {
        helmWrap.scrollIntoView({ behavior: "smooth", block: "nearest" });
      } catch (e1) {
        try {
          helmWrap.scrollIntoView(true);
        } catch (e2) {
          /* старые браузеры */
        }
      }
    });
  }

  function updateHelmProgressBar(pct, stage) {
    var p = Math.max(0, Math.min(100, Number(pct) || 0));
    if (helmBar) helmBar.style.width = p + "%";
    if (helmTrack) helmTrack.setAttribute("aria-valuenow", String(p));
    if (helmLabel) helmLabel.textContent = stage || "—";
  }

  /** Полный текст журнала в панели с теми же классами, что «Журнал задания» при создании кластера. */
  function setHelmLogText(text) {
    if (!helmLogPanel) return;
    helmLogPanel.textContent = text || "";
    helmLogPanel.scrollTop = helmLogPanel.scrollHeight;
  }

  /**
   * Блокировка форм на время синхронного запроса Helm (без полноэкранного спиннера).
   * @param {boolean} disabled
   */
  function setAddonFormsDisabled(disabled) {
    document
      .querySelectorAll(
        "#addons-cards-wrap .cluster-addon-card button, #addons-cards-wrap .cluster-addon-card input, #addons-cards-wrap .cluster-addon-card select, #addons-cards-wrap .cluster-addon-card textarea",
      )
      .forEach(function (el) {
        el.disabled = !!disabled;
      });
    if (sel) sel.disabled = !!disabled;
    var refBtn = document.getElementById("addons-refresh-status");
    if (refBtn) refBtn.disabled = !!disabled;
  }

  /** Таймер «последней надежды», если async bootstrap не дошёл до hide (ошибка до finally, зависший fetch). */
  var addonsOverlayFailsafeTimer = null;

  /** Скрыть полноэкранный спиннер первой загрузки страницы (как на панели и странице кластера). */
  function hideAddonsPageLoadingOverlay() {
    if (addonsOverlayFailsafeTimer !== null) {
      clearTimeout(addonsOverlayFailsafeTimer);
      addonsOverlayFailsafeTimer = null;
    }
    const el = document.getElementById("addons-page-loading-overlay");
    if (!el) return;
    el.classList.add("hidden");
    el.setAttribute("aria-busy", "false");
    el.setAttribute("aria-hidden", "true");
    document.body.classList.remove("cluster-addons-page-loading");
  }

  /** Страховка на случай зависшего fetch без ответа/reject. */
  function armAddonsOverlayFailsafe(ms) {
    if (addonsOverlayFailsafeTimer !== null) {
      clearTimeout(addonsOverlayFailsafeTimer);
      addonsOverlayFailsafeTimer = null;
    }
    var delay = typeof ms === "number" && ms > 0 ? ms : 40000;
    addonsOverlayFailsafeTimer = setTimeout(function () {
      addonsOverlayFailsafeTimer = null;
      var el = document.getElementById("addons-page-loading-overlay");
      if (!el || el.classList.contains("hidden")) return;
      hideAddonsPageLoadingOverlay();
      setBootstrapMsg(
        "Превышено время ожидания ответа сервера. Проверьте сеть, прокси и доступность /api/v1, затем обновите страницу.",
      );
    }, delay);
  }

  /**
   * Текстовое сообщение в полосе статуса (нет кластера / ошибка).
   * @param {string} text
   */
  function renderAddonStatusMessage(text) {
    if (!statusBar) return;
    statusBar.className = "cluster-addons-status-bar cluster-addons-status-bar--message muted";
    statusBar.replaceChildren();
    const p = document.createElement("p");
    p.className = "cluster-addons-status-msg";
    p.textContent = text;
    statusBar.appendChild(p);
  }

  /** Максимальная длина поля values_yaml на сервере (см. Pydantic max_length). */
  var VALUES_YAML_MAX_LEN = 131072;

  /**
   * Полоса под селектором кластера: только сообщения (нет кластера / ошибка), без сетки статусов.
   */
  function clearAddonToolbarStatusBar() {
    if (!statusBar) return;
    statusBar.className = "cluster-addons-status-bar";
    statusBar.replaceChildren();
  }

  /**
   * Бейджи «Установлен» / «Не установлен» в правом верхнем углу карточки аддона.
   * @param {object | null | undefined} st ответ GET …/addons/status или null при сбросе
   */
  function updateAddonCardBadges(st) {
    document.querySelectorAll(".cluster-addon-card[data-addon]").forEach(function (card) {
      var da = card.getAttribute("data-addon") || "";
      var key = ENDPOINT_TO_STATUS_KEY[da];
      var on = !!(st && key && st[key]);
      var badge = card.querySelector(".cluster-addon-card__badge");
      if (!badge) return;
      badge.textContent = on ? "Установлен" : "Не установлен";
      badge.className =
        "cluster-addon-card__badge" + (on ? " cluster-addon-card__badge--on" : " cluster-addon-card__badge--off");
      var hum = ADDON_HUMAN_LABEL[da] || da;
      badge.setAttribute("aria-label", hum + ": " + (on ? "установлен" : "не установлен"));
    });
  }

  /**
   * Прикрепить к телу POST YAML из textarea (полные values чарта). Для istio-kiali — три поля.
   * Пустой textarea → поле не отправляется (сервер соберёт defaults или пропустит -f).
   * @param {string} ep data-endpoint
   * @param {Record<string, unknown>} payload
   * @returns {boolean}
   */
  function attachAddonYamlToPayload(ep, payload) {
    if (ep === "istio-kiali") {
      var triple = [
        { id: "addons-values-yaml-istio-kiali", key: "values_yaml" },
        { id: "addons-values-yaml-istio-base", key: "istio_base_values_yaml" },
        { id: "addons-values-yaml-istio-istiod", key: "istio_istiod_values_yaml" },
      ];
      for (var i = 0; i < triple.length; i++) {
        var ta = document.getElementById(triple[i].id);
        if (!ta) continue;
        var s = String(ta.value || "").trim();
        if (!s) continue;
        if (s.length > VALUES_YAML_MAX_LEN) {
          alert("Текст YAML слишком длинный (максимум " + VALUES_YAML_MAX_LEN + " символов).");
          return false;
        }
        payload[triple[i].key] = s;
      }
      return true;
    }
    var id = "addons-values-yaml-" + ep;
    var ta2 = document.getElementById(id);
    if (!ta2) return true;
    var s2 = String(ta2.value || "").trim();
    if (!s2) return true;
    if (s2.length > VALUES_YAML_MAX_LEN) {
      alert("Текст YAML слишком длинный (максимум " + VALUES_YAML_MAX_LEN + " символов).");
      return false;
    }
    payload.values_yaml = s2;
    return true;
  }

  /** id панели с YAML (скрыта до первой загрузки по кнопке). */
  var ADDON_VALUES_PANEL_ID = {
    "ingress-nginx": "addons-values-panel-ingress-nginx",
    "kube-prometheus-stack": "addons-values-panel-kube-prometheus-stack",
    "metrics-server": "addons-values-panel-metrics-server",
    "istio-kiali": "addons-values-panel-istio-kiali",
  };

  /**
   * Скрыть блок Values и очистить textarea для одного аддона (смена версии / сброс).
   * @param {string} addon ключ data-addon / endpoint
   */
  function hideAddonValuesPanel(addon) {
    var pid = ADDON_VALUES_PANEL_ID[addon];
    if (pid) {
      var panel = document.getElementById(pid);
      if (panel) {
        panel.classList.add("hidden");
        panel.setAttribute("hidden", "hidden");
        panel.setAttribute("aria-hidden", "true");
      }
    }
    if (addon === "istio-kiali") {
      ["addons-values-yaml-istio-kiali", "addons-values-yaml-istio-base", "addons-values-yaml-istio-istiod"].forEach(
        function (tid) {
          var ta = document.getElementById(tid);
          if (ta) ta.value = "";
        },
      );
      return;
    }
    var ta = document.getElementById("addons-values-yaml-" + addon);
    if (ta) ta.value = "";
  }

  /**
   * Показать панель YAML values. Для установленного аддона при подгрузке values — блок <details> свёрнут.
   * @param {string} addon
   * @param {{ collapseDetails?: boolean } | undefined} opts
   */
  function showAddonValuesPanel(addon, opts) {
    opts = opts || {};
    var pid = ADDON_VALUES_PANEL_ID[addon];
    if (!pid) return;
    var panel = document.getElementById(pid);
    if (!panel) return;
    panel.classList.remove("hidden");
    panel.removeAttribute("hidden");
    panel.setAttribute("aria-hidden", "false");
    var det = panel.querySelector("details.cluster-addon-card__values-details");
    if (det) {
      det.open = opts.collapseDetails ? false : true;
    }
  }

  function resetAllAddonValuesPanels() {
    Object.keys(ADDON_VALUES_PANEL_ID).forEach(function (a) {
      hideAddonValuesPanel(a);
    });
  }

  /**
   * POST /helm/addons/compose-values для одной карточки: версии и поля Grafana читаются в момент нажатия кнопки.
   * @param {string} addon
   * @param {HTMLButtonElement | null} triggerBtn
   */
  async function loadComposeValuesForAddon(addon, triggerBtn) {
    setBootstrapMsg("");
    var body = { addon: addon };
    if (addon === "ingress-nginx") {
      var si = document.getElementById("addons-ing-version-select");
      if (si && si.value.trim()) body.chart_version = si.value.trim();
    } else if (addon === "kube-prometheus-stack") {
      var pv = document.getElementById("addons-prom-version-select");
      var gu = document.getElementById("addons-grafana-user");
      var gp = document.getElementById("addons-grafana-pass");
      if (pv && pv.value.trim()) body.chart_version = pv.value.trim();
      if (gu) body.grafana_admin_user = gu.value.trim() || null;
      if (gp) body.grafana_admin_password = gp.value || null;
    } else if (addon === "metrics-server") {
      var ms = document.getElementById("addons-ms-version-select");
      if (ms && ms.value.trim()) body.chart_version = ms.value.trim();
    } else if (addon === "istio-kiali") {
      var isel = document.getElementById("addons-istio-version-select");
      var ksel = document.getElementById("addons-kiali-version-select");
      if (isel && isel.value.trim()) body.istio_chart_version = isel.value.trim();
      if (ksel && ksel.value.trim()) body.kiali_chart_version = ksel.value.trim();
    } else {
      return;
    }

    var prevText = triggerBtn ? triggerBtn.textContent : "";
    if (triggerBtn) {
      triggerBtn.disabled = true;
      triggerBtn.textContent = "Загрузка…";
    }
    try {
      var r = await api("/helm/addons/compose-values", {
        method: "POST",
        headers: { "Content-Type": "application/json", Accept: "application/json" },
        body: JSON.stringify(body),
      });
      if (addon === "istio-kiali") {
        var tk = document.getElementById("addons-values-yaml-istio-kiali");
        var tb = document.getElementById("addons-values-yaml-istio-base");
        var tiod = document.getElementById("addons-values-yaml-istio-istiod");
        if (tk) tk.value = r.values_yaml || "";
        if (tb) tb.value = r.istio_base_values_yaml || "";
        if (tiod) tiod.value = r.istio_istiod_values_yaml || "";
      } else {
        var t = document.getElementById("addons-values-yaml-" + addon);
        if (t) t.value = r.values_yaml || "";
      }
      showAddonValuesPanel(addon, { collapseDetails: addonEndpointInstalled(addon) });
    } catch (e) {
      setBootstrapMsg("Не удалось загрузить values (helm show values): " + e.message);
    } finally {
      if (triggerBtn) {
        triggerBtn.disabled = false;
        triggerBtn.textContent = prevText || "Загрузить values";
      }
    }
  }

  function addonEndpointInstalled(endpoint) {
    var key = ENDPOINT_TO_STATUS_KEY[endpoint];
    return !!(lastAddonsStatus && key && lastAddonsStatus[key]);
  }

  /**
   * Текст «Установить»/«Обновить», видимость «Удалить» только при установленном аддоне; равная ширина — в CSS.
   */
  function updateAddonActionButtons() {
    document.querySelectorAll(".cluster-addon-card__actions").forEach(function (row) {
      var installBtn = row.querySelector(".btn-addon-install");
      var removeBtn = row.querySelector(".btn-addon-remove");
      if (!installBtn) return;
      var ep = installBtn.getAttribute("data-endpoint") || "";
      var on = addonEndpointInstalled(ep);
      installBtn.textContent = on ? "Обновить" : "Установить";
      var hum = ADDON_HUMAN_LABEL[ep] || ep;
      installBtn.setAttribute("aria-label", (on ? "Обновить " : "Установить ") + hum);
      if (removeBtn) {
        if (on) {
          removeBtn.classList.remove("hidden");
          removeBtn.removeAttribute("hidden");
          removeBtn.setAttribute("aria-label", "Удалить " + hum);
        } else {
          removeBtn.classList.add("hidden");
          removeBtn.setAttribute("hidden", "hidden");
          removeBtn.removeAttribute("aria-label");
        }
      }
    });
  }

  /**
   * Заполнить выпадающий список версиями чарта; первая опция — «последняя».
   * @param {HTMLSelectElement | null} selectEl
   * @param {string[] | undefined} versions
   */
  function fillVersionSelect(selectEl, versions) {
    if (!selectEl) return;
    const cur = selectEl.value;
    selectEl.innerHTML = "";
    const o0 = document.createElement("option");
    o0.value = "";
    o0.textContent = "Последняя (из репозитория)";
    selectEl.appendChild(o0);
    (versions || []).forEach(function (v) {
      if (!v) return;
      const o = document.createElement("option");
      o.value = v;
      o.textContent = v;
      selectEl.appendChild(o);
    });
    if (cur) {
      for (let i = 0; i < selectEl.options.length; i++) {
        if (selectEl.options[i].value === cur) {
          selectEl.value = cur;
          break;
        }
      }
    }
  }

  /**
   * В селекте отметить версию из кластера и подписать опцию; при отсутствии в списке репозитория — добавить опцию.
   */
  function mergeInstalledChartVersion(selectEl, version, isInstalled) {
    if (!selectEl || !isInstalled || !version) return;
    var v = String(version).trim();
    if (!v) return;
    var found = false;
    for (var i = 0; i < selectEl.options.length; i++) {
      var o = selectEl.options[i];
      if (o.value === v) {
        o.textContent = v + INSTALLED_VER_SUFFIX;
        found = true;
        break;
      }
    }
    if (!found) {
      var nu = document.createElement("option");
      nu.value = v;
      nu.textContent = v + INSTALLED_VER_SUFFIX;
      selectEl.appendChild(nu);
    }
    selectEl.value = v;
  }

  /** Подставить в селекты версии из lastAddonsStatus (после fillVersionSelect). */
  function mergeChartVersionsFromStatus(st) {
    if (!st) return;
    mergeInstalledChartVersion(
      document.getElementById("addons-ing-version-select"),
      st.ingress_nginx_chart_version,
      st.ingress_nginx,
    );
    mergeInstalledChartVersion(
      document.getElementById("addons-prom-version-select"),
      st.kube_prometheus_stack_chart_version,
      st.kube_prometheus_stack,
    );
    mergeInstalledChartVersion(
      document.getElementById("addons-ms-version-select"),
      st.metrics_server_chart_version,
      st.metrics_server,
    );
    var istioVer = st.istiod_chart_version || st.istio_base_chart_version;
    mergeInstalledChartVersion(
      document.getElementById("addons-istio-version-select"),
      istioVer,
      st.istio_mesh_ready,
    );
    mergeInstalledChartVersion(
      document.getElementById("addons-kiali-version-select"),
      st.kiali_server_chart_version,
      st.istio_mesh_ready,
    );
  }

  /**
   * Заполнить textarea из GET …/addons/installed-values; снять панели с аддонов, которые не установлены.
   * @param {object} iv ответ API
   */
  function applyInstalledValuesPayload(iv) {
    var st = lastAddonsStatus;
    if (!iv || !st) return;

    if (!st.ingress_nginx) {
      hideAddonValuesPanel("ingress-nginx");
    } else if (iv.ingress_nginx && iv.ingress_nginx.values_yaml) {
      var ti = document.getElementById("addons-values-yaml-ingress-nginx");
      if (ti) ti.value = iv.ingress_nginx.values_yaml;
      showAddonValuesPanel("ingress-nginx", { collapseDetails: true });
    }

    if (!st.kube_prometheus_stack) {
      hideAddonValuesPanel("kube-prometheus-stack");
    } else if (iv.kube_prometheus_stack && iv.kube_prometheus_stack.values_yaml) {
      var tp = document.getElementById("addons-values-yaml-kube-prometheus-stack");
      if (tp) tp.value = iv.kube_prometheus_stack.values_yaml;
      showAddonValuesPanel("kube-prometheus-stack", { collapseDetails: true });
    }

    if (!st.metrics_server) {
      hideAddonValuesPanel("metrics-server");
    } else if (iv.metrics_server && iv.metrics_server.values_yaml) {
      var tm = document.getElementById("addons-values-yaml-metrics-server");
      if (tm) tm.value = iv.metrics_server.values_yaml;
      showAddonValuesPanel("metrics-server", { collapseDetails: true });
    }

    if (!st.istio_mesh_ready) {
      hideAddonValuesPanel("istio-kiali");
    } else if (iv.istio_kiali) {
      var ik = iv.istio_kiali;
      if (ik.kiali_values_yaml) {
        var tk = document.getElementById("addons-values-yaml-istio-kiali");
        if (tk) tk.value = ik.kiali_values_yaml;
      }
      if (ik.istio_base_values_yaml) {
        var tb = document.getElementById("addons-values-yaml-istio-base");
        if (tb) tb.value = ik.istio_base_values_yaml;
      }
      if (ik.istiod_values_yaml) {
        var tiod = document.getElementById("addons-values-yaml-istio-istiod");
        if (tiod) tiod.value = ik.istiod_values_yaml;
      }
      if (ik.kiali_values_yaml || ik.istio_base_values_yaml || ik.istiod_values_yaml) {
        showAddonValuesPanel("istio-kiali", { collapseDetails: true });
      }
    }
  }

  /**
   * Версии в селектах + YAML из кластера для установленных аддонов (после статуса и списка версий репозитория).
   * @param {string} clusterName
   */
  async function maybeSyncInstalledAddonsFromCluster(clusterName) {
    if (!clusterName || !lastAddonsStatus) return;
    mergeChartVersionsFromStatus(lastAddonsStatus);
    var st = lastAddonsStatus;
    var any =
      st.ingress_nginx ||
      st.kube_prometheus_stack ||
      st.metrics_server ||
      st.istio_mesh_ready;
    if (!any) {
      resetAllAddonValuesPanels();
      return;
    }
    try {
      var iv = await api("/clusters/" + encodeURIComponent(clusterName) + "/addons/installed-values");
      applyInstalledValuesPayload(iv);
    } catch (e) {
      setBootstrapMsg("Не удалось загрузить values из кластера: " + e.message);
    }
  }

  async function loadChartVersions() {
    setBootstrapMsg("Загрузка списка версий Helm-чартов (helm repo update, может занять до минуты)…");
    try {
      const d = await api("/helm/chart-versions");
      setBootstrapMsg("");
      fillVersionSelect(document.getElementById("addons-ing-version-select"), d.ingress_nginx);
      fillVersionSelect(document.getElementById("addons-prom-version-select"), d.kube_prometheus_stack);
      fillVersionSelect(document.getElementById("addons-ms-version-select"), d.metrics_server);
      fillVersionSelect(document.getElementById("addons-istio-version-select"), d.istio);
      fillVersionSelect(document.getElementById("addons-kiali-version-select"), d.kiali_server);
      resetAllAddonValuesPanels();
      chartVersionsReady = true;
      var n = getClusterName();
      if (n) await maybeSyncInstalledAddonsFromCluster(n);
    } catch (e) {
      setBootstrapMsg("Список версий чартов недоступен: " + e.message);
    }
  }

  async function loadClusters() {
    if (!sel) return;
    try {
      const rows = await api("/clusters");
      setBootstrapMsg("");
      sel.innerHTML = "";
      const withKube = rows.filter(function (c) { return c.has_local_kubeconfig; });
      if (!withKube.length) {
        const o = document.createElement("option");
        o.value = "";
        o.textContent = "Нет кластеров с kubeconfig на диске";
        sel.appendChild(o);
        return;
      }
      const o0 = document.createElement("option");
      o0.value = "";
      o0.textContent = "— выберите кластер —";
      sel.appendChild(o0);
      withKube.forEach(function (c) {
        const o = document.createElement("option");
        o.value = c.name;
        o.textContent = c.name + (c.kind_nodes_running ? "" : " (узлы остановлены)");
        sel.appendChild(o);
      });
    } catch (e) {
      sel.innerHTML = "";
      const o = document.createElement("option");
      o.value = "";
      o.textContent = "Ошибка загрузки списка";
      sel.appendChild(o);
      setBootstrapMsg("Ошибка GET /clusters: " + e.message);
    }
  }

  async function refreshStatus() {
    const n = getClusterName();
    if (!statusBar) {
      updateAddonsCardsVisibility();
      return;
    }
    if (!n) {
      lastAddonsStatus = null;
      clearAddonToolbarStatusBar();
      updateAddonCardBadges(null);
      updateAddonActionButtons();
      updateAddonsCardsVisibility();
      resetAllAddonValuesPanels();
      return;
    }
    try {
      const st = await api("/clusters/" + encodeURIComponent(n) + "/addons/status");
      lastAddonsStatus = st;
      clearAddonToolbarStatusBar();
      updateAddonCardBadges(st);
      updateAddonActionButtons();
    } catch (e) {
      lastAddonsStatus = null;
      renderAddonStatusMessage("Статус недоступен: " + e.message);
      updateAddonCardBadges(null);
      updateAddonActionButtons();
    }
    updateAddonsCardsVisibility();
    if (chartVersionsReady && getClusterName()) {
      try {
        await maybeSyncInstalledAddonsFromCluster(getClusterName());
      } catch (e2) {
        /* уже отражено в maybeSync */
      }
    }
  }

  async function runInstall(endpoint, payload) {
    const n = getClusterName();
    if (!n) {
      alert("Выберите кластер.");
      return;
    }
    const isUpdate = addonEndpointInstalled(endpoint);
    const hum = ADDON_HUMAN_LABEL[endpoint] || endpoint;
    revealHelmProgressSection(endpoint);
    setAddonFormsDisabled(true);
    if (helmHint) {
      helmHint.textContent = isUpdate
        ? "Обновление релиза «" + hum + "» через helm в контейнере; журнал под этой карточкой аддона."
        : "Установка релиза «" + hum + "» через helm в контейнере; журнал под этой карточкой аддона.";
    }
    updateHelmProgressBar(8, isUpdate ? "Обновление Helm…" : "Установка Helm…");
    setHelmLogText(
      "Кластер: «" +
        n +
        "»\n" +
        (isUpdate ? "POST (обновление)" : "POST (установка)") +
        " /api/v1/clusters/…/addons/" +
        endpoint +
        "\n\nОжидаем завершение команды (несколько минут — норма)…",
    );
    try {
      const r = await api("/clusters/" + encodeURIComponent(n) + "/addons/" + endpoint, {
        method: "POST",
        headers: { "Content-Type": "application/json", Accept: "application/json" },
        body: payload !== undefined ? JSON.stringify(payload) : "{}",
      });
      var okLines = [
        "Кластер: «" + n + "»",
        (isUpdate ? "POST (обновление)" : "POST (установка)") + " /addons/" + endpoint,
        "",
        r.message || "OK",
      ];
      if (r.log) {
        okLines.push("");
        okLines.push(String(r.log));
      }
      setHelmLogText(okLines.join("\n"));
      updateHelmProgressBar(100, "Готово");
      if (helmHint) {
        helmHint.textContent =
          "Операция завершена. Полный журнал добавлен в файл helm_addon_log.json в каталоге кластера (история записей).";
      }
      await refreshStatus();
    } catch (e) {
      var failLines = [
        "Кластер: «" + n + "»",
        (isUpdate ? "POST (обновление)" : "POST (установка)") + " /addons/" + endpoint,
        "",
        "Ошибка: " + e.message,
      ];
      setHelmLogText(failLines.join("\n"));
      updateHelmProgressBar(100, "Ошибка");
      if (helmHint) {
        helmHint.textContent =
          "Запрос не удался. При ответе сервера об ошибке Helm в журнале кластера могла появиться запись со статусом failed.";
      }
    } finally {
      setAddonFormsDisabled(false);
    }
  }

  async function runDelete(endpoint) {
    const n = getClusterName();
    if (!n) {
      alert("Выберите кластер.");
      return;
    }
    const hum = ADDON_HUMAN_LABEL[endpoint] || endpoint;
    const ok = await openConfirmModal({
      title: "Удалить аддон?",
      message:
        "Кластер «" +
        n +
        "»: снять релиз «" +
        hum +
        "» с кластера (helm uninstall). Действие необратимо для установленных компонентов.",
      confirmLabel: "Удалить",
      danger: true,
    });
    if (!ok) return;
    revealHelmProgressSection(endpoint);
    setAddonFormsDisabled(true);
    if (helmHint) {
      helmHint.textContent =
        "Удаление релиза «" + hum + "» (helm uninstall); журнал под этой карточкой аддона.";
    }
    updateHelmProgressBar(8, "Удаление Helm…");
    setHelmLogText(
      "Кластер: «" + n + "»\nDELETE /api/v1/clusters/…/addons/" + endpoint + "\n\nОжидаем завершение команды…",
    );
    try {
      const r = await api("/clusters/" + encodeURIComponent(n) + "/addons/" + endpoint, {
        method: "DELETE",
        headers: { Accept: "application/json" },
      });
      var okDel = [
        "Кластер: «" + n + "»",
        "DELETE /addons/" + endpoint,
        "",
        r.message || "OK",
      ];
      if (r.log) {
        okDel.push("");
        okDel.push(String(r.log));
      }
      setHelmLogText(okDel.join("\n"));
      updateHelmProgressBar(100, "Готово");
      if (helmHint) {
        helmHint.textContent =
          "Операция завершена. Запись добавлена в helm_addon_log.json в каталоге кластера (история).";
      }
      await refreshStatus();
    } catch (e) {
      var failDel = [
        "Кластер: «" + n + "»",
        "DELETE /addons/" + endpoint,
        "",
        "Ошибка: " + e.message,
      ];
      setHelmLogText(failDel.join("\n"));
      updateHelmProgressBar(100, "Ошибка");
      if (helmHint) {
        helmHint.textContent =
          "Запрос не удался. При ошибке Helm на сервере в журнале кластера могла появиться запись со статусом failed.";
      }
    } finally {
      setAddonFormsDisabled(false);
    }
  }

  document.querySelectorAll(".btn-addon-install").forEach(function (btn) {
    btn.addEventListener("click", function () {
      const ep = btn.getAttribute("data-endpoint") || "";
      if (ep === "ingress-nginx") {
        const sel = document.getElementById("addons-ing-version-select");
        const v = sel && sel.value.trim();
        const payload = {};
        if (v) payload.chart_version = v;
        if (!attachAddonYamlToPayload(ep, payload)) return;
        runInstall(ep, payload);
        return;
      }
      if (ep === "kube-prometheus-stack") {
        const u = document.getElementById("addons-grafana-user");
        const p = document.getElementById("addons-grafana-pass");
        const pv = document.getElementById("addons-prom-version-select");
        const user = u ? u.value.trim() : "admin";
        const pw = p ? p.value : "";
        if (pw.length < 8) {
          alert("Задайте пароль Grafana не короче 8 символов.");
          return;
        }
        const payload = { grafana_admin_user: user, grafana_admin_password: pw };
        if (pv && pv.value.trim()) payload.chart_version = pv.value.trim();
        if (!attachAddonYamlToPayload(ep, payload)) return;
        runInstall(ep, payload);
        return;
      }
      if (ep === "metrics-server") {
        const ms = document.getElementById("addons-ms-version-select");
        const payload = {};
        if (ms && ms.value.trim()) payload.chart_version = ms.value.trim();
        if (!attachAddonYamlToPayload(ep, payload)) return;
        runInstall(ep, payload);
        return;
      }
      if (ep === "istio-kiali") {
        const isel = document.getElementById("addons-istio-version-select");
        const ksel = document.getElementById("addons-kiali-version-select");
        const payload = {};
        if (isel && isel.value.trim()) payload.istio_chart_version = isel.value.trim();
        if (ksel && ksel.value.trim()) payload.kiali_chart_version = ksel.value.trim();
        if (!attachAddonYamlToPayload(ep, payload)) return;
        runInstall(ep, payload);
      }
    });
  });

  document.querySelectorAll(".btn-addon-remove").forEach(function (btn) {
    btn.addEventListener("click", function () {
      const ep = btn.getAttribute("data-endpoint") || "";
      if (ep) runDelete(ep);
    });
  });

  document.querySelectorAll(".btn-addon-load-values").forEach(function (btn) {
    btn.addEventListener("click", function () {
      var ad = btn.getAttribute("data-addon") || "";
      if (ad) loadComposeValuesForAddon(ad, btn);
    });
  });

  const refreshBtn = document.getElementById("addons-refresh-status");
  if (refreshBtn) refreshBtn.addEventListener("click", function () { refreshStatus(); });

  if (sel) {
    sel.addEventListener("change", function () { refreshStatus(); });
  }

  bindConfirmModalControls();

  /** Смена версии — сбрасываем предпросмотр YAML (устаревший контент). */
  var versionSelectToAddon = {
    "addons-ing-version-select": "ingress-nginx",
    "addons-prom-version-select": "kube-prometheus-stack",
    "addons-ms-version-select": "metrics-server",
    "addons-istio-version-select": "istio-kiali",
    "addons-kiali-version-select": "istio-kiali",
  };
  Object.keys(versionSelectToAddon).forEach(function (id) {
    var el = document.getElementById(id);
    if (el) {
      el.addEventListener("change", function () {
        hideAddonValuesPanel(versionSelectToAddon[id]);
      });
    }
  });
  /* Логин/пароль Grafana: если правили после загрузки YAML — скрываем блок, чтобы не путать с устаревшим предпросмотром. */
  var grafanaUserEl = document.getElementById("addons-grafana-user");
  var grafanaPassEl = document.getElementById("addons-grafana-pass");
  function onGrafanaCredChange() {
    hideAddonValuesPanel("kube-prometheus-stack");
  }
  if (grafanaUserEl) grafanaUserEl.addEventListener("input", onGrafanaCredChange);
  if (grafanaPassEl) grafanaPassEl.addEventListener("input", onGrafanaCredChange);

  /**
   * Оверлей убираем после списка кластеров и статуса — без ожидания GET /helm/chart-versions.
   * Кластеры/статус оборачиваем в таймаут: иначе при «висящем» fetch await не завершается и finally не вызывается.
   */
  armAddonsOverlayFailsafe(45000);

  (async function bootstrapAddonsPage() {
    try {
      await promiseWithTimeout(
        loadClusters(),
        30000,
        "Таймаут: список кластеров не получен за 30 с. Проверьте GET /api/v1/clusters.",
      );
      await promiseWithTimeout(
        refreshStatus(),
        30000,
        "Таймаут: статус аддонов не получен за 30 с.",
      );
    } catch (e) {
      var msg0 = e && e.message ? e.message : String(e);
      setBootstrapMsg(msg0);
    } finally {
      hideAddonsPageLoadingOverlay();
    }
    try {
      await promiseWithTimeout(
        loadChartVersions(),
        180000,
        "Таймаут: версии Helm-чартов не получены за 3 мин. (helm repo update).",
      );
    } catch (e) {
      var msg1 = e && e.message ? e.message : String(e);
      setBootstrapMsg("Не удалось загрузить версии чартов: " + msg1);
    }
  })();
})();
