(() => {
  const PAGE_SIZE = 10;

  const ICONS = {
    io: `<svg class="ioModulesMirIcon" width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><rect x="3" y="6" width="14" height="8" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="7" cy="10" r="1.2" fill="currentColor"/><circle cx="13" cy="10" r="1.2" fill="currentColor"/><path d="M10 3v3M10 14v3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>`,
    edit: `<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M9.5 2.5l2 2L5 11H3v-2L9.5 2.5z" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>`,
    delete: `<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M4 4l6 6M10 4l-6 6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>`,
  };

  const el = (id) => document.getElementById(id);
  const t = (key, vars) => window.I18n?.t(key, vars) ?? key;

  const listEl = el("ioModuleList");
  const emptyEl = el("ioModuleListEmpty");
  const tableEl = el("ioModulesTable");
  const filterInputEl = el("ioModulesFilterInput");
  const filterCountEl = el("ioModulesFilterCount");
  const pageLabelEl = el("ioModulesPageLabel");
  const dialogEl = el("ioModuleEditDialog");
  const formEl = el("ioModuleEditForm");
  const titleEl = el("ioModuleEditTitle");
  const deleteBtnEl = el("ioModuleEditDeleteBtn");
  const typeHintEl = el("ioModuleEditTypeHint");
  const deleteConfirmDialogEl = el("ioModuleDeleteConfirmDialog");
  const deleteConfirmTextEl = el("ioModuleDeleteConfirmText");

  const fields = {
    site: el("ioModuleEditSite"),
    name: el("ioModuleEditName"),
    type: el("ioModuleEditType"),
    ip: el("ioModuleEditIp"),
  };

  const store = {
    modules: [],
    sites: [],
    editingId: null,
    pendingDeleteId: null,
    filter: "",
    page: 1,
  };

  function canWrite() {
    if (!window.AuthApp?.canWrite) return true;
    return window.AuthApp.canWrite("integrations");
  }

  function escapeHtml(str) {
    return String(str)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");
  }

  async function apiJson(url, opts = {}) {
    const res = await fetch(url, { credentials: "include", ...opts });
    const text = await res.text();
    let data = null;
    try {
      data = text ? JSON.parse(text) : null;
    } catch {
      data = null;
    }
    if (!res.ok) throw new Error((data && data.error) || text || res.statusText);
    return data;
  }

  function defaultSiteId() {
    return store.sites[0]?.id || "site_configuration";
  }

  function typeLabel(type) {
    if (type === "bluetooth") return t("ioModules.type.bluetooth");
    if (type === "wise") return t("ioModules.type.wise");
    return type || "—";
  }

  function statusLabel(mod) {
    return mod.connected ? t("ioModules.status.connected") : t("ioModules.status.disconnected");
  }

  function updateTypeHint() {
    if (!typeHintEl || !fields.type) return;
    const type = fields.type.value;
    typeHintEl.textContent =
      type === "wise" ? t("ioModules.typeHint.wise") : t("ioModules.typeHint.bluetooth");
  }

  async function refreshAll() {
    const [sitesData, modulesData] = await Promise.all([
      apiJson("/api/sites"),
      apiJson("/api/io_modules"),
    ]);
    store.sites = Array.isArray(sitesData.sites) ? sitesData.sites : [];
    store.modules = Array.isArray(modulesData.io_modules) ? modulesData.io_modules : [];
    return store.modules;
  }

  function moduleById(id) {
    return store.modules.find((m) => m.id === id) || null;
  }

  function filteredModules() {
    const q = store.filter.trim().toLowerCase();
    let items = [...store.modules].sort((a, b) => (a.name || "").localeCompare(b.name || ""));
    if (q) {
      items = items.filter((m) => {
        const name = (m.name || "").toLowerCase();
        const ip = (m.ip_address || "").toLowerCase();
        const type = (m.module_type || "").toLowerCase();
        return name.includes(q) || ip.includes(q) || type.includes(q);
      });
    }
    return items;
  }

  function pageCount(total) {
    return Math.max(1, Math.ceil(total / PAGE_SIZE));
  }

  function pagedItems(items) {
    const totalPages = pageCount(items.length);
    if (store.page > totalPages) store.page = totalPages;
    if (store.page < 1) store.page = 1;
    const start = (store.page - 1) * PAGE_SIZE;
    return items.slice(start, start + PAGE_SIZE);
  }

  function updatePagerUi(totalItems) {
    const totalPages = pageCount(totalItems);
    if (filterCountEl) filterCountEl.textContent = t("ioModules.itemsFound", { n: totalItems });
    if (pageLabelEl) pageLabelEl.textContent = t("ioModules.pageOf", { page: store.page, total: totalPages });
    const atStart = store.page <= 1;
    const atEnd = store.page >= totalPages;
    el("ioModulesPageFirst")?.toggleAttribute("disabled", atStart);
    el("ioModulesPagePrev")?.toggleAttribute("disabled", atStart);
    el("ioModulesPageNext")?.toggleAttribute("disabled", atEnd);
    el("ioModulesPageLast")?.toggleAttribute("disabled", atEnd);
  }

  function fillSiteSelect(selectedId) {
    if (!fields.site) return;
    fields.site.innerHTML = "";
    store.sites.forEach((site) => {
      const opt = document.createElement("option");
      opt.value = site.id;
      opt.textContent = site.name || site.id;
      if (site.id === selectedId) opt.selected = true;
      fields.site.appendChild(opt);
    });
    if (!fields.site.value && store.sites[0]) fields.site.value = store.sites[0].id;
  }

  function renderList() {
    if (!listEl) return;
    const items = filteredModules();
    const pageItems = pagedItems(items);
    updatePagerUi(items.length);

    listEl.innerHTML = "";
    const showEmpty = items.length === 0;
    if (tableEl) tableEl.hidden = showEmpty;
    if (emptyEl) {
      emptyEl.hidden = !showEmpty;
      emptyEl.textContent = store.filter.trim() ? t("ioModules.emptyFilter") : t("ioModules.empty");
    }

    pageItems.forEach((mod) => {
      const tr = document.createElement("tr");
      tr.className = "mapsMirRow ioModulesMirRow";
      tr.dataset.id = mod.id;

      const actions = canWrite()
        ? `<div class="mapsMirRowActions">
            <button type="button" class="mapsMirIconBtn ioModuleEditBtn" data-edit="${escapeHtml(mod.id)}" title="${escapeHtml(t("common.edit"))}">${ICONS.edit}</button>
            <button type="button" class="mapsMirIconBtn mapsMirIconBtn--danger ioModuleDeleteBtn" data-delete="${escapeHtml(mod.id)}" title="${escapeHtml(t("common.delete"))}">${ICONS.delete}</button>
          </div>`
        : "";

      const statusClass = mod.connected ? "ioModulesMirStatus--on" : "ioModulesMirStatus--off";

      tr.innerHTML = `
        <td class="ioModulesMirCellIcon">${ICONS.io}</td>
        <td class="ioModulesMirCellName">
          <button type="button" class="mapsMirNameLink ioModulesMirNameLink" data-edit="${escapeHtml(mod.id)}">${escapeHtml(mod.name || "—")}</button>
        </td>
        <td>${escapeHtml(typeLabel(mod.module_type))}</td>
        <td>${escapeHtml(mod.ip_address || "—")}</td>
        <td><span class="ioModulesMirStatus ${statusClass}">${escapeHtml(statusLabel(mod))}</span></td>
        <td class="mapsMirCellFunctions">${actions}</td>
      `;
      listEl.appendChild(tr);
    });
  }

  function openCreateDialog() {
    store.editingId = null;
    if (titleEl) titleEl.textContent = t("ioModules.createTitle");
    fillSiteSelect(defaultSiteId());
    if (fields.name) fields.name.value = "";
    if (fields.type) fields.type.value = "bluetooth";
    if (fields.ip) fields.ip.value = "";
    updateTypeHint();
    deleteBtnEl?.toggleAttribute("hidden", true);
    dialogEl?.showModal();
  }

  function openEditDialog(id) {
    const mod = moduleById(id);
    if (!mod) return;
    store.editingId = id;
    if (titleEl) titleEl.textContent = t("ioModules.editTitle");
    fillSiteSelect(mod.site_id || defaultSiteId());
    if (fields.name) fields.name.value = mod.name || "";
    if (fields.type) fields.type.value = mod.module_type || "bluetooth";
    if (fields.ip) fields.ip.value = mod.ip_address || "";
    updateTypeHint();
    deleteBtnEl?.toggleAttribute("hidden", false);
    dialogEl?.showModal();
  }

  async function saveModule(evt) {
    evt?.preventDefault();
    const payload = {
      site_id: fields.site?.value || defaultSiteId(),
      name: (fields.name?.value || "").trim(),
      module_type: fields.type?.value || "bluetooth",
      ip_address: (fields.ip?.value || "").trim(),
    };
    if (!payload.name || !payload.ip_address) {
      alert(t("ioModules.error.missing"));
      return;
    }
    try {
      if (store.editingId) {
        await apiJson(`/api/io_modules/${encodeURIComponent(store.editingId)}`, {
          method: "PUT",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(payload),
        });
      } else {
        await apiJson("/api/io_modules", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(payload),
        });
      }
      dialogEl?.close();
      await refreshAll();
      renderList();
      window.dispatchEvent(new CustomEvent("lm:io-modules-changed"));
    } catch (e) {
      alert(e.message);
    }
  }

  function openDeleteConfirm(id) {
    const mod = moduleById(id);
    if (!mod) return;
    store.pendingDeleteId = id;
    if (deleteConfirmTextEl) {
      deleteConfirmTextEl.textContent = t("ioModules.deleteConfirmText", { name: mod.name || "" });
    }
    deleteConfirmDialogEl?.showModal();
  }

  async function confirmDelete() {
    const id = store.pendingDeleteId;
    if (!id) return;
    try {
      const res = await fetch(`/api/io_modules/${encodeURIComponent(id)}`, { method: "DELETE", credentials: "include" });
      if (!res.ok) {
        let msg = res.statusText;
        try {
          const err = await res.json();
          if (err.error) msg = err.error;
          if (err.usages) msg += "\n" + JSON.stringify(err.usages, null, 2);
        } catch {
          /* ignore */
        }
        throw new Error(msg);
      }
      store.pendingDeleteId = null;
      deleteConfirmDialogEl?.close();
      dialogEl?.close();
      await refreshAll();
      renderList();
      window.dispatchEvent(new CustomEvent("lm:io-modules-changed"));
    } catch (e) {
      alert(e.message);
    }
  }

  function clearFilters() {
    store.filter = "";
    store.page = 1;
    if (filterInputEl) filterInputEl.value = "";
    renderList();
  }

  function bindEvents() {
    el("ioModuleCreateBtn")?.addEventListener("click", () => {
      if (canWrite()) openCreateDialog();
    });
    formEl?.addEventListener("submit", saveModule);
    el("ioModuleEditCancelBtn")?.addEventListener("click", () => dialogEl?.close());
    deleteBtnEl?.addEventListener("click", () => {
      if (store.editingId) openDeleteConfirm(store.editingId);
    });
    fields.type?.addEventListener("change", updateTypeHint);
    el("ioModuleTestBtn")?.addEventListener("click", async () => {
      const ip = (fields.ip?.value || "").trim();
      if (!ip) {
        alert(t("ioModules.error.missing"));
        return;
      }
      try {
        await apiJson("/api/io_modules/test", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ ip_address: ip, port: 502 }),
        });
        alert(t("ioModules.testOk"));
      } catch (e) {
        alert(e.message);
      }
    });

    listEl?.addEventListener("click", (evt) => {
      const editBtn = evt.target.closest("[data-edit]");
      const deleteBtn = evt.target.closest("[data-delete]");
      if (editBtn?.dataset.edit && canWrite()) openEditDialog(editBtn.dataset.edit);
      else if (deleteBtn?.dataset.delete && canWrite()) openDeleteConfirm(deleteBtn.dataset.delete);
    });

    deleteConfirmDialogEl?.addEventListener("cancel", (evt) => {
      evt.preventDefault();
      store.pendingDeleteId = null;
      deleteConfirmDialogEl?.close();
    });
    el("ioModuleDeleteCancelBtn")?.addEventListener("click", () => {
      store.pendingDeleteId = null;
      deleteConfirmDialogEl?.close();
    });
    el("ioModuleDeleteYesBtn")?.addEventListener("click", confirmDelete);

    filterInputEl?.addEventListener("input", () => {
      store.filter = filterInputEl.value;
      store.page = 1;
      renderList();
    });
    el("ioModulesClearFiltersBtn")?.addEventListener("click", clearFilters);
    el("ioModulesPageFirst")?.addEventListener("click", () => {
      store.page = 1;
      renderList();
    });
    el("ioModulesPagePrev")?.addEventListener("click", () => {
      store.page = Math.max(1, store.page - 1);
      renderList();
    });
    el("ioModulesPageNext")?.addEventListener("click", () => {
      store.page += 1;
      renderList();
    });
    el("ioModulesPageLast")?.addEventListener("click", () => {
      store.page = pageCount(filteredModules().length);
      renderList();
    });
    el("ioModulesHelpBtn")?.addEventListener("click", () => alert(t("ioModules.helpBody")));

    window.addEventListener("lm:locale-change", () => renderList());
  }

  async function onPageShow() {
    if (!window.AuthApp?.canAccessPage?.("io-modules")) return;
    document.body.classList.toggle("auth-readonly-io-modules", !canWrite());
    try {
      await refreshAll();
      renderList();
    } catch (e) {
      if (emptyEl) {
        emptyEl.hidden = false;
        emptyEl.textContent = e.message;
      }
      if (tableEl) tableEl.hidden = true;
    }
  }

  function onPageHide() {
    dialogEl?.close();
    deleteConfirmDialogEl?.close();
  }

  bindEvents();

  window.IoModulesCatalog = {
    getModules: () => [...store.modules],
    getNames: () => store.modules.map((m) => m.name).filter(Boolean),
    refresh: refreshAll,
  };

  window.IoModulesApp = { onPageShow, onPageHide, refreshAll, getModules: () => [...store.modules] };
})();
