(() => {
  const STORAGE_KEY = "phenikaax_missions_v1";

  const ACTION_GROUPS = {
    Move: [
      { type: "move_to_position", label: "Go to position" },
      { type: "move_to_marker", label: "Go to marker" },
      { type: "adjust_localization", label: "Adjust localization" },
      { type: "switch_map", label: "Switch map" },
      { type: "wait", label: "Wait" },
      { type: "set_speed", label: "Set speed" },
    ],
    Logic: [
      { type: "if", label: "If" },
      { type: "while", label: "While", isLoop: true },
      { type: "loop", label: "Loop", isLoop: true },
      { type: "break", label: "Break" },
      { type: "continue", label: "Continue" },
      { type: "pause", label: "Pause" },
    ],
    "I/O": [
      { type: "set_digital_output", label: "Set digital output" },
      { type: "wait_digital_input", label: "Wait for digital input" },
      { type: "connect_bluetooth", label: "Connect Bluetooth module" },
      { type: "disconnect_bluetooth", label: "Disconnect Bluetooth module" },
      { type: "set_plc_register", label: "Set PLC register" },
    ],
    Cart: [
      { type: "pick_cart", label: "Pick cart" },
      { type: "drop_cart", label: "Drop cart" },
    ],
    Misc: [
      { type: "user_log", label: "User log" },
      { type: "play_sound", label: "Play sound" },
    ],
  };

  const DEFAULT_GROUPS = ["Missions", "Move", "Logic", "I/O", "Cart", "Misc"];

  const SAMPLE_POSITIONS = ["Charging station", "Warehouse", "Production line 1", "Dock A"];
  const SAMPLE_MARKERS = ["Marker 1", "Marker 2", "Home"];
  const SAMPLE_IO_MODULES = ["GPIO module 1", "PLC I/O 1"];
  let ioModuleCatalog = [];

  function ioModuleNames() {
    const names = ioModuleCatalog.map((m) => m.name).filter(Boolean);
    return names.length ? names : SAMPLE_IO_MODULES;
  }

  function ioModuleByName(name) {
    return ioModuleCatalog.find((m) => m.name === name || m.id === name) || null;
  }

  function ioOutputPorts(moduleName) {
    const mod = ioModuleByName(moduleName);
    return mod?.module_type === "wise" ? [0, 1, 2, 3] : [1, 2, 3, 4];
  }

  function ioInputPorts(moduleName) {
    return ioOutputPorts(moduleName);
  }

  async function loadIoModuleCatalog() {
    try {
      if (window.IoModulesCatalog?.refresh) {
        ioModuleCatalog = await window.IoModulesCatalog.refresh();
      } else {
        const res = await fetch("/api/io_modules", { credentials: "include" });
        if (res.ok) {
          const data = await res.json();
          ioModuleCatalog = Array.isArray(data.io_modules) ? data.io_modules : [];
        }
      }
    } catch {
      ioModuleCatalog = [];
    }
    return ioModuleCatalog;
  }
  const SAMPLE_CARTS = ["Any valid cart", "Cart A", "Cart B"];

  let missionPositionCatalog = [];

  function buildPositionCatalogFromMaps(maps) {
    const out = [];
    (maps || []).forEach((map) => {
      if (!map?.id) return;
      const zones = Array.isArray(map.zones) ? map.zones : [];
      zones
        .filter((z) => z && z.type === "position" && typeof z.id === "string" && z.id)
        .forEach((z) => {
          const mapName = map.name || map.id;
          const pname = z.name || z.id;
          out.push({
            position_id: z.id,
            map_id: map.id,
            name: pname,
            label: `${mapName} / ${pname}`,
          });
        });
    });
    return out;
  }

  function positionOptions() {
    return missionPositionCatalog.map((p) => ({ value: p.position_id, label: p.label }));
  }

  function positionIds() {
    return missionPositionCatalog.map((p) => p.position_id);
  }

  function positionLabel(ref) {
    if (!ref) return "—";
    const hit = missionPositionCatalog.find((p) => p.position_id === ref || p.name === ref);
    return hit?.label || ref;
  }

  function positionSelectOptions() {
    const opts = positionOptions();
    if (opts.length) return opts;
    return SAMPLE_POSITIONS.map((n) => ({ value: n, label: n }));
  }

  function syncPositionFieldDefs() {
    const ids = positionIds();
    ["move_to_position", "adjust_localization", "if", "pick_cart", "drop_cart"].forEach((type) => {
      const defs = VARIABLE_FIELD_DEFS[type];
      if (!defs) return;
      defs.forEach((def) => {
        if (def.key === "position") def.options = ids.length ? ids : SAMPLE_POSITIONS;
      });
    });
  }

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

  const missionListEl = el("missionList");
  const missionListEmptyEl = el("missionListEmpty");
  const missionsListViewEl = el("missionsListView");
  const missionEditorViewEl = el("missionEditorView");
  const missionActionBarEl = el("missionActionBar");
  const missionGroupTabsEl = el("missionGroupTabs");
  const missionActionListEl = el("missionActionList");
  const missionEditorTitleEl = el("missionEditorTitle");
  const missionEditorMetaEl = el("missionEditorMeta");
  const missionEditorDirtyEl = el("missionEditorDirty");
  const missionCreateDialogEl = el("missionCreateDialog");
  const missionSettingsDialogEl = el("missionSettingsDialog");
  const missionSaveAsDialogEl = el("missionSaveAsDialog");
  const missionActionConfigDialogEl = el("missionActionConfigDialog");
  const missionActionConfigBodyEl = el("missionActionConfigBody");
  const missionActionConfigTitleEl = el("missionActionConfigTitle");
  const missionQueueListEl = el("missionQueueList");
  const missionQueueEmptyEl = el("missionQueueEmpty");
  const missionQueueRunnerEl = el("missionQueueRunner");
  const missionQueueDialogEl = el("missionQueueDialog");
  const missionQueueVarFieldsEl = el("missionQueueVarFields");
  const missionQueueDialogMissionEl = el("missionQueueDialogMission");
  const missionQueueVarHintEl = el("missionQueueVarHint");

  const VARIABLE_FIELD_DEFS = {
    move_to_position: [{ key: "position", label: "Position", options: SAMPLE_POSITIONS }],
    adjust_localization: [{ key: "position", label: "Position", options: SAMPLE_POSITIONS }],
    move_to_marker: [{ key: "marker", label: "Marker", options: SAMPLE_MARKERS }],
    if: [{ key: "position", label: "Position", options: SAMPLE_POSITIONS }],
    pick_cart: [
      { key: "position", label: "Position", options: SAMPLE_POSITIONS },
      { key: "cart", label: "Cart", options: SAMPLE_CARTS },
    ],
    drop_cart: [{ key: "position", label: "Position", options: SAMPLE_POSITIONS }],
    switch_map: [{ key: "map_id", label: "Map", options: [] }],
  };

  const store = {
    missions: [],
    groups: [...DEFAULT_GROUPS],
    editingId: null,
    draft: null,
    dirty: false,
    drag: null,
    configActionId: null,
    configListPath: "root",
    queue: [],
    runner: { state: "idle", message: "" },
    pendingQueueMissionId: null,
  };

  let queuePollRefs = 0;
  let queuePollTimer = null;

  function newId() {
    if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
    return `m_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
  }

  function defaultParams(type) {
    switch (type) {
      case "move_to_position":
        return { position: positionIds()[0] || SAMPLE_POSITIONS[0], check_free: true };
      case "move_to_marker":
        return { marker: SAMPLE_MARKERS[0] };
      case "adjust_localization":
        return { position: positionIds()[0] || SAMPLE_POSITIONS[0] };
      case "wait":
        return { seconds: 1 };
      case "set_speed":
        return { speed: "normal" };
      case "switch_map":
        return { map_id: "", entry_position_id: "" };
      case "if":
        return { condition: "io_input", module: ioModuleNames()[0] || "", input: 1, expected: true };
      case "while":
        return { condition: "io_input", module: ioModuleNames()[0] || "", input: 1, expected: true };
      case "loop":
        return { count: 1, mode: "count" };
      case "set_digital_output":
        return { module: ioModuleNames()[0] || "", output: 1, value: true, operation: "set", timeout_ms: 0 };
      case "wait_digital_input":
        return { module: ioModuleNames()[0] || "", input: 1, expected: true, timeout_s: 30 };
      case "connect_bluetooth":
      case "disconnect_bluetooth":
        return { module: ioModuleNames()[0] || "" };
      case "set_plc_register":
        return { register: 1, action: "set", value: 0 };
      case "pick_cart":
        return { position: positionIds()[0] || SAMPLE_POSITIONS[0], cart: SAMPLE_CARTS[0] };
      case "drop_cart":
        return { position: positionIds()[0] || SAMPLE_POSITIONS[0], collision_check: true };
      case "user_log":
        return { message: "Mission step" };
      case "play_sound":
        return { sound: "sys_beep", volume: 100 };
      default:
        return {};
    }
  }

  function actionMeta(type) {
    for (const items of Object.values(ACTION_GROUPS)) {
      const hit = items.find((a) => a.type === type);
      if (hit) return { ...hit, label: t(`missions.action.${type}`) || hit.label };
    }
    return { type, label: t(`missions.action.${type}`) || type };
  }

  function createAction(type, overrides = {}) {
    const meta = actionMeta(type);
    return {
      id: newId(),
      kind: "action",
      type,
      label: meta.label,
      params: defaultParams(type),
      children: meta.isLoop ? [] : undefined,
      ...overrides,
    };
  }

  function createMissionRef(mission) {
    return {
      id: newId(),
      kind: "mission",
      refId: mission.id,
      label: mission.name,
      params: {},
    };
  }

  function createMission(name, group, description) {
    return {
      id: newId(),
      name: name.trim(),
      group: group.trim() || "Missions",
      description: (description || "").trim(),
      actions: [],
      updated_at: new Date().toISOString(),
    };
  }

  function loadStoreLocal() {
    try {
      const raw = localStorage.getItem(STORAGE_KEY);
      if (!raw) return;
      const data = JSON.parse(raw);
      if (Array.isArray(data.missions)) store.missions = data.missions;
      if (Array.isArray(data.groups)) store.groups = data.groups;
    } catch {
      /* ignore */
    }
    ensureDefaultGroups();
  }

  async function loadStoreFromBackend() {
    try {
      const res = await fetch("/api/missions", { credentials: "include" });
      if (!res.ok) return false;
      const data = await res.json();
      if (Array.isArray(data.missions)) store.missions = data.missions;
      if (Array.isArray(data.groups)) store.groups = data.groups;
      ensureDefaultGroups();
      localStorage.setItem(
        STORAGE_KEY,
        JSON.stringify({ missions: store.missions, groups: store.groups })
      );
      return true;
    } catch {
      return false;
    }
  }

  let persistTimer = null;

  async function syncStoreToBackend() {
    try {
      await fetch("/api/missions", {
        credentials: "include",
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ missions: store.missions, groups: store.groups }),
      });
    } catch {
      /* ignore */
    }
  }

  function loadStore() {
    loadStoreLocal();
  }

  function persistStore() {
    localStorage.setItem(
      STORAGE_KEY,
      JSON.stringify({ missions: store.missions, groups: store.groups })
    );
    clearTimeout(persistTimer);
    persistTimer = setTimeout(syncStoreToBackend, 400);
  }

  function ensureDefaultGroups() {
    DEFAULT_GROUPS.forEach((g) => {
      if (!store.groups.includes(g)) store.groups.push(g);
    });
  }

  function allGroups() {
    const fromMissions = store.missions.map((m) => m.group).filter(Boolean);
    return [...new Set([...store.groups, ...fromMissions])].sort((a, b) => a.localeCompare(b));
  }

  function fillGroupSelect(selectEl, selected) {
    if (!selectEl) return;
    selectEl.innerHTML = "";
    allGroups().forEach((g) => {
      const opt = document.createElement("option");
      opt.value = g;
      opt.textContent = g;
      if (g === selected) opt.selected = true;
      selectEl.appendChild(opt);
    });
  }

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

  function getDraft() {
    return store.draft;
  }

  function setDirty(flag) {
    store.dirty = !!flag;
    if (missionEditorDirtyEl) missionEditorDirtyEl.hidden = !store.dirty;
  }

  function actionSummary(action) {
    if (action.kind === "mission") return `Mission con: ${action.label}`;
    const p = action.params || {};
    const fmtVar = (key, val) => (p[`${key}_var`] ? `${val} (biến)` : val);
    switch (action.type) {
      case "move_to_position":
        return `Position: ${fmtVar("position", positionLabel(p.position))}${p.check_free ? " • kiểm tra trống" : ""}`;
      case "move_to_marker":
        return `Marker: ${fmtVar("marker", p.marker)}`;
      case "wait":
        return `${p.seconds}s`;
      case "set_speed":
        return `Speed: ${p.speed}`;
      case "loop":
        return p.mode === "endless" ? "Lặp vô hạn" : `Lặp ${p.count} lần • ${action.children?.length || 0} bước`;
      case "if": {
        if (p.condition === "io_input") {
          const port = p.input != null ? p.input : p.pin;
          return `If I/O ${p.module} in ${port} = ${p.expected ? "ON" : "OFF"}`;
        }
        return `If ${p.condition} @ ${positionLabel(p.position)}`;
      }
      case "while":
        return `While I/O ${p.module} in ${p.input != null ? p.input : p.pin} = ${p.expected ? "ON" : "OFF"} • ${action.children?.length || 0} bước`;
      case "set_digital_output": {
        const port = p.output != null ? p.output : p.pin;
        const op = p.operation === "timed" ? ` (${p.timeout_ms || 0}ms)` : "";
        return `${p.module} out ${port} → ${p.value ? "ON" : "OFF"}${op}`;
      }
      case "wait_digital_input": {
        const port = p.input != null ? p.input : p.pin;
        return `${p.module} in ${port} = ${p.expected ? "ON" : "OFF"} (${p.timeout_s}s)`;
      }
      case "connect_bluetooth":
        return `Connect ${p.module || "?"}`;
      case "disconnect_bluetooth":
        return `Disconnect ${p.module || "?"}`;
      case "set_plc_register":
        return `Reg ${p.register}: ${p.action} ${p.value}`;
      case "pick_cart":
      case "drop_cart":
        return `${action.type === "pick_cart" ? "Pick" : "Drop"} @ ${fmtVar("position", positionLabel(p.position))}${action.type === "pick_cart" ? ` • ${fmtVar("cart", p.cart)}` : ""}`;
      case "user_log":
        return p.message || "—";
      case "switch_map":
        return `${p.map_id || "—"}`;
      case "play_sound": {
        const soundName = (() => {
          const s = (window.SoundsApp?.getSounds?.() || []).find((x) => x.id === p.sound);
          return s?.name || p.sound || "—";
        })();
        return `${soundName} · ${p.volume != null ? p.volume : 100}%`;
      }
      default:
        return action.label;
    }
  }

  function normalizeActionTree(actions) {
    if (!Array.isArray(actions)) return;
    actions.forEach((action) => {
      if ((action.type === "loop" || action.type === "while") && !Array.isArray(action.children)) {
        action.children = [];
      }
      if (action.type === "if") {
        if (!Array.isArray(action.children)) action.children = [];
        if (!Array.isArray(action.else_children)) action.else_children = [];
      }
      if (Array.isArray(action.children)) normalizeActionTree(action.children);
      if (Array.isArray(action.else_children)) normalizeActionTree(action.else_children);
    });
  }

  function findActionList(path) {
    const draft = getDraft();
    if (!draft) return null;
    if (path === "root") return draft.actions;
    const parts = path.split(".").filter((p) => p !== "root");
    let list = draft.actions;
    let i = 0;
    while (i < parts.length) {
      const part = parts[i];
      if (part === "else") return null;
      const node = list.find((a) => a.id === part);
      if (!node) return null;
      i += 1;
      if (i < parts.length && parts[i] === "else") {
        if (!Array.isArray(node.else_children)) return null;
        list = node.else_children;
        i += 1;
        continue;
      }
      if (i >= parts.length) {
        if (!Array.isArray(node.children)) return null;
        return node.children;
      }
      if (!Array.isArray(node.children)) return null;
      list = node.children;
    }
    return list;
  }

  function findActionWithParent(actionId, list = getDraft()?.actions, path = "root", parent = null) {
    if (!list) return null;
    for (let i = 0; i < list.length; i += 1) {
      const action = list[i];
      if (action.id === actionId) return { action, list, index: i, path, parent };
      if (Array.isArray(action.children)) {
        const hit = findActionWithParent(actionId, action.children, `${path}.${action.id}`, action);
        if (hit) return hit;
      }
      if (Array.isArray(action.else_children)) {
        const hit = findActionWithParent(actionId, action.else_children, `${path}.${action.id}.else`, action);
        if (hit) return hit;
      }
    }
    return null;
  }

  function removeActionFromTree(actionId) {
    const hit = findActionWithParent(actionId);
    if (!hit) return false;
    hit.list.splice(hit.index, 1);
    setDirty(true);
    return true;
  }

  function moveAction(actionId, targetPath, targetIndex) {
    const hit = findActionWithParent(actionId);
    if (!hit) return false;
    const targetList = findActionList(targetPath);
    if (!targetList) return false;

    if (hit.path === targetPath) {
      const [item] = hit.list.splice(hit.index, 1);
      const idx = hit.index < targetIndex ? targetIndex - 1 : targetIndex;
      targetList.splice(idx, 0, item);
      setDirty(true);
      return true;
    }

    const movingIntoLoop = targetPath !== "root";
    if (movingIntoLoop) {
      const loopId = targetPath.split(".").pop();
      if (loopId === actionId) return false;
      let cursor = findActionWithParent(loopId);
      while (cursor) {
        if (cursor.action.id === actionId) return false;
        cursor = cursor.parent ? findActionWithParent(cursor.parent.id) : null;
      }
    }

    const [item] = hit.list.splice(hit.index, 1);
    targetList.splice(targetIndex, 0, item);
    setDirty(true);
    return true;
  }

  function addActionToList(type, listPath = "root") {
    const list = findActionList(listPath);
    if (!list) return;
    list.push(createAction(type));
    setDirty(true);
    renderMissionEditor();
  }

  function addMissionRefToList(missionId, listPath = "root") {
    const mission = findMission(missionId);
    const list = findActionList(listPath);
    if (!mission || !list || mission.id === store.editingId) return;
    list.push(createMissionRef(mission));
    setDirty(true);
    renderMissionEditor();
  }

  async function missionApi(path, opts = {}) {
    if (window.AuthApp && !window.AuthApp.isReady()) {
      throw new Error("not authenticated");
    }
    const res = await fetch(path, {
      credentials: "include",
      headers: { "Content-Type": "application/json", ...(opts.headers || {}) },
      ...opts,
    });
    if (!res.ok) {
      let msg = res.statusText;
      try {
        const err = await res.json();
        if (err.error) msg = err.error;
      } catch {
        /* ignore */
      }
      throw new Error(msg);
    }
    if (res.status === 204) return null;
    return res.json();
  }

  function walkActions(actions, visit, depth = 0) {
    if (!Array.isArray(actions) || depth > 12) return;
    actions.forEach((action) => {
      visit(action, depth);
      if (Array.isArray(action.children)) walkActions(action.children, visit, depth + 1);
      if (Array.isArray(action.else_children)) walkActions(action.else_children, visit, depth + 1);
    });
  }

  function resolveActionSnapshot(action, depth = 0) {
    const copy = JSON.parse(JSON.stringify(action));
    if (copy.kind === "mission") {
      const ref = findMission(copy.refId);
      if (ref && depth < 8) copy.resolved_mission = resolveMissionSnapshot(ref, depth + 1);
    }
    if (Array.isArray(copy.children)) {
      copy.children = copy.children.map((child) => resolveActionSnapshot(child, depth));
    }
    if (Array.isArray(copy.else_children)) {
      copy.else_children = copy.else_children.map((child) => resolveActionSnapshot(child, depth));
    }
    return copy;
  }

  function resolveMissionSnapshot(mission, depth = 0) {
    const copy = JSON.parse(JSON.stringify(mission));
    copy.actions = (copy.actions || []).map((a) => resolveActionSnapshot(a, depth));
    return copy;
  }

  function collectMissionVariables(mission) {
    const vars = [];
    walkActions(mission.actions, (action) => {
      if (action.kind === "mission" || !action.params) return;
      const defs = VARIABLE_FIELD_DEFS[action.type] || [];
      defs.forEach((def) => {
        if (!action.params[`${def.key}_var`]) return;
        vars.push({
          key: `${action.id}:${def.key}`,
          label: `${action.label} — ${def.label}`,
          options: def.options,
          default: action.params[def.key] || def.options[0] || "",
        });
      });
    });
    return vars;
  }

  function formatQueueParameters(entry) {
    const params = entry.parameters || {};
    const keys = Object.keys(params);
    if (!keys.length) return "";
    return keys
      .map((key) => `<span class="missionQueueParamVar">${escapeHtml(params[key])}</span>`)
      .join(" • ");
  }

  function queueStatusLabel(status) {
    const map = {
      pending: "missions.queue.status.pending",
      executing: "missions.queue.status.executing",
      completed: "missions.queue.status.done",
      failed: "missions.queue.status.error",
      cancelled: "missions.queue.status.cancelled",
    };
    const key = map[status];
    return key ? t(key) : status;
  }

  async function refreshQueue() {
    if (window.AuthApp && !window.AuthApp.isReady()) return;
    try {
      const data = await missionApi("/api/mission_queue");
      store.queue = Array.isArray(data.queue) ? data.queue : [];
      store.runner = data.runner && typeof data.runner === "object" ? data.runner : { state: "idle", message: "" };
      renderQueuePanel();
      notifyQueueUpdate();
    } catch (e) {
      if (String(e.message || "").includes("not authenticated")) return;
      if (missionQueueRunnerEl) missionQueueRunnerEl.textContent = `${t("common.error", { msg: e.message })}`;
    }
  }

  const queueListeners = new Set();
  function notifyQueueUpdate() {
    queueListeners.forEach((fn) => {
      try {
        fn(getQueueSnapshot());
      } catch {
        /* ignore */
      }
    });
  }

  function getQueueSnapshot() {
    return {
      queue: JSON.parse(JSON.stringify(store.queue)),
      runner: JSON.parse(JSON.stringify(store.runner)),
    };
  }

  function renderQueueInto(target, options = {}) {
    const listEl = target.listEl;
    const runnerEl = target.runnerEl;
    const emptyEl = target.emptyEl;
    const compact = !!options.compact;
    if (!listEl) return;

    listEl.innerHTML = "";
    if (emptyEl) emptyEl.hidden = store.queue.length > 0;

    if (runnerEl) {
      const st = store.runner.state || "idle";
      runnerEl.classList.toggle("running", st === "running" || st === "paused");
      runnerEl.classList.toggle("paused", st === "paused");
      const action = store.runner.current_action ? ` • ${store.runner.current_action}` : "";
      runnerEl.textContent = store.runner.message
        ? `${store.runner.message}${action}`
        : st === "idle"
          ? compact
            ? t("missions.queue.ready")
            : t("missions.queue.idleMessage")
          : "—";
    }

    store.queue.forEach((entry, index) => {
      const row = document.createElement("div");
      row.className = `missionQueueItem status-${entry.status || "pending"}${compact ? " compact" : ""}`;
      const paramHtml = formatQueueParameters(entry);
      const canReorder = entry.status === "pending" && !compact;
      row.innerHTML = compact
        ? `
        <div>
          <div class="missionQueueItemTitle">${escapeHtml(entry.mission_name || "Mission")}</div>
          <div class="missionQueueItemMeta">${queueStatusLabel(entry.status)} • #${index + 1}</div>
          ${paramHtml ? `<div class="missionQueueItemParams">${paramHtml}</div>` : ""}
        </div>
        <div class="missionQueueWidgetActions">
          ${entry.status === "pending" ? `<button type="button" class="iconBtn danger" data-queue-remove="${entry.id}" title="" data-i18n-title="common.delete">×</button>` : `<span class="missionQueueStatus ${escapeHtml(entry.status || "pending")}">${queueStatusLabel(entry.status)}</span>`}
        </div>`
        : `
        <div class="missionQueueOrder">
          <button type="button" class="iconBtn" data-queue-up="${entry.id}" title="${t("missions.queue.moveUp")}" ${canReorder && index > 0 ? "" : "disabled"}>↑</button>
          <button type="button" class="iconBtn" data-queue-down="${entry.id}" title="${t("missions.queue.moveDown")}" ${canReorder && index < store.queue.length - 1 ? "" : "disabled"}>↓</button>
        </div>
        <div>
          <div class="missionQueueItemTitle">${escapeHtml(entry.mission_name || "Mission")}</div>
          <div class="missionQueueItemMeta">${escapeHtml(entry.mission_group || "")} • #${index + 1}</div>
          ${paramHtml ? `<div class="missionQueueItemParams">${paramHtml}</div>` : ""}
        </div>
        <div style="display:flex;flex-direction:column;align-items:flex-end;gap:6px;">
          <span class="missionQueueStatus ${escapeHtml(entry.status || "pending")}">${queueStatusLabel(entry.status)}</span>
          ${entry.status === "pending" ? `<button type="button" class="btn subtle danger" data-queue-remove="${entry.id}">${t("common.delete")}</button>` : ""}
        </div>`;

      row.querySelector("[data-queue-up]")?.addEventListener("click", () => moveQueueItem(entry.id, -1));
      row.querySelector("[data-queue-down]")?.addEventListener("click", () => moveQueueItem(entry.id, 1));
      row.querySelector("[data-queue-remove]")?.addEventListener("click", () => removeQueueItem(entry.id));
      listEl.appendChild(row);
    });
  }

  function renderQueuePanel() {
    renderQueueInto(
      {
        listEl: missionQueueListEl,
        runnerEl: missionQueueRunnerEl,
        emptyEl: missionQueueEmptyEl,
      },
      { compact: false }
    );
  }

  async function moveQueueItem(id, delta) {
    const ids = store.queue.map((q) => q.id);
    const idx = ids.indexOf(id);
    if (idx < 0) return;
    if (store.queue[idx].status !== "pending") return;
    const next = idx + delta;
    if (next < 0 || next >= store.queue.length) return;
    if (store.queue[next].status !== "pending") return;
    [ids[idx], ids[next]] = [ids[next], ids[idx]];
    try {
      await missionApi("/api/mission_queue/reorder", {
        method: "PUT",
        body: JSON.stringify({ ordered_ids: ids }),
      });
      await refreshQueue();
    } catch (e) {
      alert(e.message);
    }
  }

  async function removeQueueItem(id) {
    try {
      await missionApi(`/api/mission_queue/${id}`, { method: "DELETE" });
      await refreshQueue();
    } catch (e) {
      alert(e.message);
    }
  }

  async function clearQueue() {
    if (!confirm(t("missions.queue.clearConfirm"))) return;
    try {
      await missionApi("/api/mission_queue", { method: "DELETE" });
      await refreshQueue();
    } catch (e) {
      alert(e.message);
    }
  }

  async function enqueueMission(missionId, parameters = {}) {
    const mission = findMission(missionId);
    if (!mission) throw new Error("Mission không tồn tại");
    const payload = {
      mission: resolveMissionSnapshot(mission),
      parameters,
    };
    await missionApi("/api/mission_queue", { method: "POST", body: JSON.stringify(payload) });
    await refreshQueue();
  }

  async function pauseRunner() {
    await missionApi("/api/mission_queue/pause", { method: "POST", body: "{}" });
    await refreshQueue();
  }

  async function continueRunner() {
    await missionApi("/api/mission_queue/continue", { method: "POST", body: "{}" });
    await refreshQueue();
  }

  async function cancelRunner() {
    if (!confirm(t("missions.queue.cancelConfirm"))) return;
    await missionApi("/api/mission_queue/cancel", { method: "POST", body: "{}" });
    await refreshQueue();
  }

  function openQueueDialog(missionId) {
    const mission = findMission(missionId);
    if (!mission) return;
    store.pendingQueueMissionId = missionId;
    const vars = collectMissionVariables(mission);
    if (missionQueueDialogMissionEl) {
      missionQueueDialogMissionEl.textContent = `Mission: ${mission.name}`;
    }
    if (missionQueueVarFieldsEl) {
      missionQueueVarFieldsEl.innerHTML = "";
      if (!vars.length) {
        missionQueueVarFieldsEl.innerHTML = `<p class="mutedNote">Mission không có tham số biến. Bấm «Thêm vào queue» để chạy.</p>`;
      } else {
        vars.forEach((v) => {
          const row = document.createElement("div");
          row.className = "row rowWide";
          const lab = document.createElement("label");
          lab.textContent = v.label;
          const sel = document.createElement("select");
          sel.dataset.varKey = v.key;
          v.options.forEach((opt) => {
            const o = document.createElement("option");
            const value = opt?.value ?? opt;
            const label = opt?.label ?? positionLabel(value);
            o.value = value;
            o.textContent = label;
            if (value === v.default) o.selected = true;
            sel.appendChild(o);
          });
          row.appendChild(lab);
          row.appendChild(sel);
          missionQueueVarFieldsEl.appendChild(row);
        });
      }
    }
    if (missionQueueVarHintEl) missionQueueVarHintEl.hidden = vars.length === 0;
    missionQueueDialogEl.showModal();
  }

  async function submitQueueDialog(evt) {
    evt.preventDefault();
    const mission = findMission(store.pendingQueueMissionId);
    if (!mission) return;
    const parameters = {};
    missionQueueVarFieldsEl?.querySelectorAll("[data-var-key]").forEach((sel) => {
      parameters[sel.dataset.varKey] = sel.value;
    });
    const payload = {
      mission: resolveMissionSnapshot(mission),
      parameters,
    };
    try {
      await missionApi("/api/mission_queue", { method: "POST", body: JSON.stringify(payload) });
      missionQueueDialogEl.close();
      store.pendingQueueMissionId = null;
      await refreshQueue();
    } catch (e) {
      alert(e.message);
    }
  }

  function startQueuePoll() {
    if (window.AuthApp && !window.AuthApp.isReady()) return;
    queuePollRefs += 1;
    if (queuePollRefs === 1) {
      refreshQueue();
      queuePollTimer = setInterval(refreshQueue, 1500);
    }
  }

  function stopQueuePoll() {
    if (queuePollRefs <= 0) return;
    queuePollRefs -= 1;
    if (queuePollRefs === 0 && queuePollTimer) {
      clearInterval(queuePollTimer);
      queuePollTimer = null;
    }
  }

  function stopQueuePollForce() {
    queuePollRefs = 0;
    if (queuePollTimer) {
      clearInterval(queuePollTimer);
      queuePollTimer = null;
    }
  }

  function renderMissionList() {
    if (!missionListEl) return;
    missionListEl.innerHTML = "";
    const items = [...store.missions].sort((a, b) => a.name.localeCompare(b.name));
    missionListEmptyEl.hidden = items.length > 0;

    items.forEach((mission) => {
      const row = document.createElement("div");
      row.className = "missionListItem";
      row.innerHTML = `
        <div>
          <div class="missionListItemTitle">${escapeHtml(mission.name)}</div>
          <div class="missionListItemMeta">${escapeHtml(mission.group)} • ${mission.actions.length} action(s)${mission.description ? ` • ${escapeHtml(mission.description)}` : ""}</div>
        </div>
        <div class="missionListItemActions">
          <button type="button" class="iconBtn missionQueueBtn" data-queue="${mission.id}" title="Thêm vào mission queue" aria-label="Thêm vào queue">▤</button>
          <button type="button" class="btn subtle" data-edit="${mission.id}">${t("common.edit")}</button>
          <button type="button" class="btn subtle danger" data-delete="${mission.id}">${t("common.delete")}</button>
        </div>`;
      row.addEventListener("click", (evt) => {
        if (evt.target.closest("button")) return;
        openEditor(mission.id);
      });
      row.querySelector("[data-queue]").addEventListener("click", (evt) => {
        evt.stopPropagation();
        openQueueDialog(mission.id);
      });
      row.querySelector("[data-edit]").addEventListener("click", (evt) => {
        evt.stopPropagation();
        openEditor(mission.id);
      });
      row.querySelector("[data-delete]").addEventListener("click", (evt) => {
        evt.stopPropagation();
        if (!confirm(t("missions.deleteConfirm", { name: mission.name }))) return;
        store.missions = store.missions.filter((m) => m.id !== mission.id);
        persistStore();
        renderMissionList();
      });
      missionListEl.appendChild(row);
    });
  }

  function groupLabel(name) {
    const key = `missions.group.${name}`;
    const v = t(key);
    return v !== key ? v : name;
  }

  function renderActionPalette() {
    if (!missionGroupTabsEl) return;
    missionGroupTabsEl.innerHTML = "";

    Object.entries(ACTION_GROUPS).forEach(([groupName, actions]) => {
      const tab = document.createElement("div");
      tab.className = "missionGroupTab";

      const btn = document.createElement("button");
      btn.type = "button";
      btn.className = "missionGroupTabBtn";
      btn.textContent = groupName;
      btn.dataset.group = groupName;

      const menu = document.createElement("div");
      menu.className = "missionGroupMenu";
      menu.hidden = true;

      actions.forEach((def) => {
        const item = document.createElement("button");
        item.type = "button";
        item.className = "missionPaletteItem";
        item.draggable = true;
        item.innerHTML = `<span class="missionActionIcon">${def.isLoop ? "↻" : "▶"}</span><span>${escapeHtml(def.label)}</span>`;
        item.addEventListener("click", () => {
          addActionToList(def.type, "root");
          menu.hidden = true;
          btn.classList.remove("open");
        });
        item.addEventListener("dragstart", (evt) => {
          onPaletteDragStart(evt, { actionType: def.type });
        });
        item.addEventListener("dragend", onDragEnd);
        menu.appendChild(item);
      });

      const embeddable = store.missions.filter((m) => m.id !== store.editingId);
      if (embeddable.length) {
        const sep = document.createElement("div");
        sep.className = "mutedNote";
        sep.style.padding = "6px 10px";
        sep.textContent = "Missions có sẵn";
        menu.appendChild(sep);
        embeddable.forEach((m) => {
          const item = document.createElement("button");
          item.type = "button";
          item.className = "missionPaletteItem missionRef";
          item.draggable = true;
          item.innerHTML = `<span class="missionActionIcon kind-mission">◎</span><span>${escapeHtml(m.name)}</span>`;
          item.addEventListener("click", () => {
            addMissionRefToList(m.id, "root");
            menu.hidden = true;
            btn.classList.remove("open");
          });
          item.addEventListener("dragstart", (evt) => {
            onPaletteDragStart(evt, { missionId: m.id });
          });
          item.addEventListener("dragend", onDragEnd);
          menu.appendChild(item);
        });
      }

      btn.addEventListener("click", (evt) => {
        evt.stopPropagation();
        const open = !menu.hidden;
        closeAllPaletteMenus();
        menu.hidden = open;
        btn.classList.toggle("open", !open);
      });

      tab.appendChild(btn);
      tab.appendChild(menu);
      missionGroupTabsEl.appendChild(tab);
    });
  }

  function closeAllPaletteMenus() {
    document.querySelectorAll(".missionGroupMenu").forEach((m) => {
      m.hidden = true;
    });
    document.querySelectorAll(".missionGroupTabBtn.open").forEach((b) => b.classList.remove("open"));
  }

  function renderActionRows(actions, listPath, container) {
    actions.forEach((action, index) => {
      const row = document.createElement("div");
      row.className = "missionActionRow";
      row.dataset.actionId = action.id;
      row.dataset.listPath = listPath;
      row.dataset.index = String(index);

      const iconClass =
        action.kind === "mission" ? "kind-mission" : action.type === "loop" || action.type === "while" ? "kind-loop" : action.type === "if" ? "kind-if" : "";
      const iconChar = action.kind === "mission" ? "◎" : action.type === "loop" || action.type === "while" ? "↻" : action.type === "if" ? "?" : "▶";

      row.innerHTML = `
        <div class="missionDragHandle" draggable="true" title="Kéo để sắp xếp" aria-label="Kéo để sắp xếp">↕</div>
        <div class="missionActionTop">
          <div class="missionActionMain">
            <div class="missionActionLabelRow">
              <span class="missionActionIcon ${iconClass}">${iconChar}</span>
              <span class="missionActionLabel">${escapeHtml(action.label)}</span>
            </div>
            <div class="missionActionSummary">${escapeHtml(actionSummary(action))}</div>
          </div>
          <div class="missionActionBtns">
            <button type="button" class="iconBtn" data-config="${action.id}" title="Cấu hình">⚙</button>
            <button type="button" class="iconBtn danger" data-remove="${action.id}" title="" data-i18n-title="common.delete">×</button>
          </div>
        </div>`;

      if ((action.type === "loop" || action.type === "while") && Array.isArray(action.children)) {
        const loop = document.createElement("div");
        loop.className = "missionLoopBlock";
        loop.innerHTML = `<div class="missionLoopLabel">${action.type === "while" ? "While body" : "Loop body"} — kéo action vào đây</div>`;
        const drop = document.createElement("div");
        drop.className = "missionLoopDrop";
        drop.dataset.loopPath = `${listPath}.${action.id}`;
        if (!action.children.length) {
          const empty = document.createElement("div");
          empty.className = "missionLoopEmpty";
          empty.textContent = "Kéo action hoặc mission vào đây";
          drop.appendChild(empty);
        } else {
          renderActionRows(action.children, `${listPath}.${action.id}`, drop);
        }
        loop.appendChild(drop);
        row.appendChild(loop);
      }

      if (action.type === "if" && Array.isArray(action.children)) {
        const makeBranch = (label, branchPath, children) => {
          const block = document.createElement("div");
          block.className = "missionLoopBlock missionIfBlock";
          block.innerHTML = `<div class="missionLoopLabel">${label}</div>`;
          const drop = document.createElement("div");
          drop.className = "missionLoopDrop";
          drop.dataset.loopPath = branchPath;
          if (!children.length) {
            const empty = document.createElement("div");
            empty.className = "missionLoopEmpty";
            empty.textContent = "Kéo action vào nhánh này";
            drop.appendChild(empty);
          } else {
            renderActionRows(children, branchPath, drop);
          }
          block.appendChild(drop);
          return block;
        };
        row.appendChild(makeBranch("Then", `${listPath}.${action.id}`, action.children));
        row.appendChild(
          makeBranch("Else", `${listPath}.${action.id}.else`, Array.isArray(action.else_children) ? action.else_children : [])
        );
      }

      row.querySelector("[data-config]").addEventListener("click", (evt) => {
        evt.stopPropagation();
        openActionConfig(action.id);
      });
      row.querySelector("[data-remove]").addEventListener("click", (evt) => {
        evt.stopPropagation();
        removeActionFromTree(action.id);
        renderMissionEditor();
      });

      const handle = row.querySelector(".missionDragHandle");
      handle.addEventListener("dragstart", (evt) => onDragStart(evt, action.id, listPath));
      handle.addEventListener("dragend", onDragEnd);
      row.addEventListener("dragover", onRowDragOver);
      row.addEventListener("dragleave", onRowDragLeave);
      row.addEventListener("drop", onRowDrop);

      const loopDrop = row.querySelector(".missionLoopDrop");
      if (loopDrop) {
        loopDrop.addEventListener("dragover", onLoopDragOver);
        loopDrop.addEventListener("dragleave", onLoopDragLeave);
        loopDrop.addEventListener("drop", onLoopDrop);
      }

      container.appendChild(row);
    });
  }

  function renderMissionEditor() {
    const draft = getDraft();
    if (!draft) return;
    missionEditorTitleEl.textContent = draft.name;
    missionEditorMetaEl.textContent = `${draft.group}${draft.description ? ` • ${draft.description}` : ""}`;
    renderActionPalette();
    missionActionListEl.innerHTML = "";
    renderActionRows(draft.actions, "root", missionActionListEl);
  }

  function onPaletteDragStart(evt, payload) {
    store.drag = { mode: "palette", ...payload };
    evt.dataTransfer.effectAllowed = "copy";
    evt.dataTransfer.setData("text/plain", payload.actionType || payload.missionId || "palette");
    evt.currentTarget.classList.add("dragging");
  }

  function onDragStart(evt, actionId, listPath) {
    store.drag = { actionId, listPath, mode: "reorder" };
    evt.dataTransfer.effectAllowed = "move";
    evt.dataTransfer.setData("text/plain", actionId);
    evt.target.closest(".missionActionRow")?.classList.add("dragging");
  }

  function onDragEnd(evt) {
    document.querySelectorAll(".missionActionRow.dragging, .missionActionRow.dropBefore, .missionActionRow.dropAfter").forEach((n) => {
      n.classList.remove("dragging", "dropBefore", "dropAfter");
    });
    document.querySelectorAll(".missionPaletteItem.dragging").forEach((n) => n.classList.remove("dragging"));
    document.querySelectorAll(".missionLoopDrop.dragOver").forEach((n) => n.classList.remove("dragOver"));
    store.drag = null;
  }

  function onRowDragOver(evt) {
    if (!store.drag || store.drag.mode !== "reorder") return;
    evt.preventDefault();
    const row = evt.currentTarget;
    const rect = row.getBoundingClientRect();
    const before = evt.clientY < rect.top + rect.height / 2;
    row.classList.toggle("dropBefore", before);
    row.classList.toggle("dropAfter", !before);
  }

  function onRowDragLeave(evt) {
    evt.currentTarget.classList.remove("dropBefore", "dropAfter");
  }

  function onRowDrop(evt) {
    evt.preventDefault();
    evt.stopPropagation();
    if (!store.drag || store.drag.mode !== "reorder") return;
    const row = evt.currentTarget;
    const targetPath = row.dataset.listPath;
    let targetIndex = Number(row.dataset.index);
    if (row.classList.contains("dropAfter")) targetIndex += 1;
    row.classList.remove("dropBefore", "dropAfter");
    moveAction(store.drag.actionId, targetPath, targetIndex);
    renderMissionEditor();
  }

  function onLoopDragOver(evt) {
    evt.preventDefault();
    evt.stopPropagation();
    evt.currentTarget.classList.add("dragOver");
  }

  function onLoopDragLeave(evt) {
    evt.currentTarget.classList.remove("dragOver");
  }

  function onLoopDrop(evt) {
    evt.preventDefault();
    evt.stopPropagation();
    evt.currentTarget.classList.remove("dragOver");
    if (!store.drag) return;
    const loopPath = evt.currentTarget.dataset.loopPath;
    const list = findActionList(loopPath);
    if (!list) return;

    if (store.drag.mode === "reorder") {
      moveAction(store.drag.actionId, loopPath, list.length);
    } else if (store.drag.mode === "palette") {
      if (store.drag.missionId) addMissionRefToList(store.drag.missionId, loopPath);
      else if (store.drag.actionType) {
        list.push(createAction(store.drag.actionType));
        setDirty(true);
      }
    }
    renderMissionEditor();
  }

  function openEditor(missionId) {
    const mission = findMission(missionId);
    if (!mission) return;
    store.editingId = missionId;
    store.draft = JSON.parse(JSON.stringify(mission));
    normalizeActionTree(store.draft.actions);
    setDirty(false);
    missionsListViewEl.hidden = true;
    missionEditorViewEl.hidden = false;
    renderMissionEditor();
  }

  function closeEditor() {
    if (store.dirty && !confirm(t("missions.editor.discardConfirm"))) return;
    store.editingId = null;
    store.draft = null;
    setDirty(false);
    missionEditorViewEl.hidden = true;
    missionsListViewEl.hidden = false;
    renderMissionList();
  }

  function saveDraft() {
    const draft = getDraft();
    if (!draft) return false;
    if (!draft.name.trim()) {
      alert(t("missions.error.nameRequired"));
      return false;
    }
    draft.updated_at = new Date().toISOString();
    const idx = store.missions.findIndex((m) => m.id === draft.id);
    if (idx >= 0) store.missions[idx] = JSON.parse(JSON.stringify(draft));
    else store.missions.push(JSON.parse(JSON.stringify(draft)));
    persistStore();
    setDirty(false);
    renderMissionList();
    return true;
  }

  function saveDraftAs(newName) {
    const draft = getDraft();
    if (!draft) return false;
    const name = newName.trim();
    if (!name) return false;
    if (store.missions.some((m) => m.name === name && m.id !== draft.id)) {
      alert(t("missions.error.nameDuplicate"));
      return false;
    }
    const copy = JSON.parse(JSON.stringify(draft));
    copy.id = newId();
    copy.name = name;
    copy.updated_at = new Date().toISOString();
    store.missions.push(copy);
    persistStore();
    store.editingId = copy.id;
    store.draft = copy;
    setDirty(false);
    renderMissionEditor();
    renderMissionList();
    return true;
  }

  function openCreateDialog() {
    fillGroupSelect(el("missionCreateGroup"), "Missions");
    el("missionCreateName").value = "";
    el("missionCreateGroupNew").value = "";
    el("missionCreateDesc").value = "";
    missionCreateDialogEl.showModal();
  }

  function openSettingsDialog() {
    const draft = getDraft();
    if (!draft) return;
    fillGroupSelect(el("missionSettingsGroup"), draft.group);
    el("missionSettingsName").value = draft.name;
    el("missionSettingsDesc").value = draft.description || "";
    missionSettingsDialogEl.showModal();
  }

  function openSaveAsDialog() {
    const draft = getDraft();
    if (!draft) return;
    el("missionSaveAsName").value = `${draft.name} (copy)`;
    missionSaveAsDialogEl.showModal();
  }

  function openActionConfig(actionId) {
    const hit = findActionWithParent(actionId);
    if (!hit || hit.action.kind === "mission") return;
    store.configActionId = actionId;
    missionActionConfigTitleEl.textContent = `Cấu hình: ${hit.action.label}`;
    missionActionConfigBodyEl.innerHTML = buildConfigForm(hit.action);
    missionActionConfigDialogEl.showModal();
  }

  function buildConfigForm(action) {
    const p = action.params || {};
    const grid = document.createElement("div");
    grid.className = "missionConfigGrid";

    const addField = (label, node) => {
      const row = document.createElement("div");
      row.className = "row rowWide";
      const lab = document.createElement("label");
      lab.textContent = label;
      row.appendChild(lab);
      row.appendChild(node);
      grid.appendChild(row);
    };

    const textInput = (key, value, type = "text") => {
      const input = document.createElement("input");
      input.type = type;
      input.dataset.param = key;
      input.value = value ?? "";
      return input;
    };

    const selectInput = (key, value, options) => {
      const select = document.createElement("select");
      select.dataset.param = key;
      options.forEach((opt) => {
        const o = document.createElement("option");
        o.value = opt;
        o.textContent = opt;
        if (opt === value) o.selected = true;
        select.appendChild(o);
      });
      return select;
    };

    const selectInputLabeled = (key, value, options) => {
      const select = document.createElement("select");
      select.dataset.param = key;
      options.forEach((opt) => {
        const o = document.createElement("option");
        o.value = opt.value;
        o.textContent = opt.label;
        if (opt.value === value) o.selected = true;
        select.appendChild(o);
      });
      return select;
    };

    const addVariableToggle = (paramKey, fieldLabel) => {
      const chk = document.createElement("label");
      chk.innerHTML = `<input type="checkbox" data-param="${paramKey}_var" ${p[`${paramKey}_var`] ? "checked" : ""} /> Biến — hỏi khi thêm vào queue`;
      addField(`${fieldLabel} (biến)`, chk);
    };

    switch (action.type) {
      case "move_to_position":
      case "adjust_localization":
        addField("Position", selectInputLabeled("position", p.position, positionSelectOptions()));
        addVariableToggle("position", "Position");
        if (action.type === "move_to_position") {
          const chk = document.createElement("label");
          chk.innerHTML = `<input type="checkbox" data-param="check_free" ${p.check_free ? "checked" : ""} /> Kiểm tra vị trí trống`;
          addField("Tuỳ chọn", chk);
        }
        break;
      case "move_to_marker":
        addField("Marker", selectInput("marker", p.marker, SAMPLE_MARKERS));
        addVariableToggle("marker", "Marker");
        break;
      case "wait":
        addField("Giây", textInput("seconds", p.seconds, "number"));
        break;
      case "set_speed":
        addField("Tốc độ", selectInput("speed", p.speed, ["slow", "normal", "fast"]));
        break;
      case "loop":
        addField("Chế độ", selectInput("mode", p.mode, ["count", "endless"]));
        addField("Số lần lặp", textInput("count", p.count, "number"));
        break;
      case "if":
        addField("Điều kiện", selectInput("condition", p.condition, ["io_input", "position_free", "position_occupied", "register_equals"]));
        if (p.condition === "io_input") {
          addField("Module", selectInput("module", p.module, ioModuleNames()));
          addField("Input", selectInput("input", String(p.input != null ? p.input : p.pin || 1), ioInputPorts(p.module).map(String)));
          {
            const chk = document.createElement("label");
            chk.innerHTML = `<input type="checkbox" data-param="expected" ${p.expected ? "checked" : ""} /> ${t("missions.action.waitOnLevel")}`;
            addField("Kỳ vọng", chk);
          }
        } else {
          addField("Position", selectInputLabeled("position", p.position, positionSelectOptions()));
          addVariableToggle("position", "Position");
        }
        break;
      case "while":
        addField("Điều kiện", selectInput("condition", p.condition || "io_input", ["io_input"]));
        addField("Module", selectInput("module", p.module, ioModuleNames()));
        addField("Input", selectInput("input", String(p.input != null ? p.input : p.pin || 1), ioInputPorts(p.module).map(String)));
        {
          const chk = document.createElement("label");
          chk.innerHTML = `<input type="checkbox" data-param="expected" ${p.expected ? "checked" : ""} /> ${t("missions.action.waitOnLevel")}`;
          addField("Kỳ vọng", chk);
        }
        break;
      case "set_digital_output":
        addField("Module", selectInput("module", p.module, ioModuleNames()));
        addField("Output", selectInput("output", String(p.output != null ? p.output : p.pin || 1), ioOutputPorts(p.module).map(String)));
        addField("Operation", selectInput("operation", p.operation || "set", ["set", "timed"]));
        addField("Timeout (ms)", textInput("timeout_ms", p.timeout_ms != null ? p.timeout_ms : 0, "number"));
        {
          const chk = document.createElement("label");
          chk.innerHTML = `<input type="checkbox" data-param="value" ${p.value ? "checked" : ""} /> Bật (ON)`;
          addField("Trạng thái", chk);
        }
        break;
      case "wait_digital_input":
        addField("Module", selectInput("module", p.module, ioModuleNames()));
        addField("Input", selectInput("input", String(p.input != null ? p.input : p.pin || 1), ioInputPorts(p.module).map(String)));
        addField("Timeout (s)", textInput("timeout_s", p.timeout_s, "number"));
        {
          const chk = document.createElement("label");
          chk.innerHTML = `<input type="checkbox" data-param="expected" ${p.expected ? "checked" : ""} /> ${t("missions.action.waitOnLevel")}`;
          addField("Kỳ vọng", chk);
        }
        break;
      case "connect_bluetooth":
      case "disconnect_bluetooth":
        addField("Module", selectInput("module", p.module, ioModuleNames()));
        break;
      case "set_plc_register":
        addField("Register", textInput("register", p.register, "number"));
        addField("Hành động", selectInput("action", p.action, ["set", "add", "subtract"]));
        addField("Giá trị", textInput("value", p.value, "number"));
        break;
      case "pick_cart":
        addField("Position", selectInputLabeled("position", p.position, positionSelectOptions()));
        addVariableToggle("position", "Position");
        addField("Cart", selectInput("cart", p.cart, SAMPLE_CARTS));
        addVariableToggle("cart", "Cart");
        break;
      case "drop_cart":
        addField("Position", selectInputLabeled("position", p.position, positionSelectOptions()));
        addVariableToggle("position", "Position");
        {
          const chk = document.createElement("label");
          chk.innerHTML = `<input type="checkbox" data-param="collision_check" ${p.collision_check ? "checked" : ""} /> Kiểm tra va chạm`;
          addField("An toàn", chk);
        }
        break;
      case "user_log":
        addField("Message", textInput("message", p.message));
        break;
      case "play_sound": {
        const catalog = (window.SoundsApp?.getSounds?.() || []).filter((s) => s.enabled !== false);
        const options = catalog.length
          ? catalog.map((s) => ({ value: s.id, label: s.name }))
          : [
              { value: "sys_beep", label: "Beep" },
              { value: "sys_horn", label: "Horn" },
              { value: "sys_chime", label: "Chime" },
            ];
        addField("Sound", selectInputLabeled("sound", p.sound || options[0].value, options));
        addField("Volume (0–100)", textInput("volume", p.volume != null ? p.volume : 100, "number"));
        break;
      }
      case "switch_map": {
        const options = (VARIABLE_FIELD_DEFS.switch_map?.[0]?.options || []).map((id) => ({ value: id, label: id }));
        addField("Map", selectInputLabeled("map_id", p.map_id || options[0]?.value || "", options));
        addField("Entry position id", textInput("entry_position_id", p.entry_position_id || ""));
        break;
      }
      default:
        grid.innerHTML = `<p class="mutedNote">Action này không có tham số cấu hình.</p>`;
    }
    return grid.outerHTML;
  }

  function applyActionConfig() {
    const hit = findActionWithParent(store.configActionId);
    if (!hit) return;
    const params = { ...hit.action.params };
    missionActionConfigBodyEl.querySelectorAll("[data-param]").forEach((node) => {
      const key = node.dataset.param;
      if (node.type === "checkbox") params[key] = node.checked;
      else if (node.type === "number") params[key] = Number(node.value);
      else params[key] = node.value;
    });
    hit.action.params = params;
    if (params.input != null && params.input !== "") params.input = Number(params.input);
    if (params.output != null && params.output !== "") params.output = Number(params.output);
    setDirty(true);
    renderMissionEditor();
  }

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

  function bindEvents() {
    el("missionCreateOpenBtn")?.addEventListener("click", openCreateDialog);

    el("missionCreateForm")?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      const name = el("missionCreateName").value.trim();
      if (!name) return;
      let group = el("missionCreateGroup").value;
      const groupNew = el("missionCreateGroupNew").value.trim();
      if (groupNew) {
        group = groupNew;
        if (!store.groups.includes(group)) store.groups.push(group);
      }
      if (store.missions.some((m) => m.name === name)) {
        alert(t("missions.error.nameDuplicate"));
        return;
      }
      const mission = createMission(name, group, el("missionCreateDesc").value);
      store.missions.push(mission);
      persistStore();
      missionCreateDialogEl.close();
      renderMissionList();
      openEditor(mission.id);
    });

    el("missionEditorBackBtn")?.addEventListener("click", closeEditor);
    el("missionSaveBtn")?.addEventListener("click", () => {
      if (saveDraft()) alert(t("missions.saveSuccess"));
    });
    el("missionSaveAsBtn")?.addEventListener("click", openSaveAsDialog);
    el("missionSettingsBtn")?.addEventListener("click", openSettingsDialog);

    el("missionSettingsForm")?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      const draft = getDraft();
      if (!draft) return;
      draft.name = el("missionSettingsName").value.trim();
      draft.group = el("missionSettingsGroup").value;
      draft.description = el("missionSettingsDesc").value.trim();
      if (!draft.name) {
        alert(t("missions.error.nameEmpty"));
        return;
      }
      setDirty(true);
      missionSettingsDialogEl.close();
      renderMissionEditor();
    });

    el("missionSaveAsForm")?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      if (saveDraftAs(el("missionSaveAsName").value.trim())) {
        missionSaveAsDialogEl.close();
      }
    });

    el("missionActionConfigForm")?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      applyActionConfig();
      missionActionConfigDialogEl.close();
    });

    document.querySelectorAll("[data-close-dialog]").forEach((btn) => {
      btn.addEventListener("click", () => {
        const id = btn.getAttribute("data-close-dialog");
        el(id)?.close();
      });
    });

    document.addEventListener("click", (evt) => {
      if (!evt.target.closest(".missionGroupTab")) closeAllPaletteMenus();
    });

    el("missionQueueClearBtn")?.addEventListener("click", clearQueue);
    el("missionQueueCancelBtn")?.addEventListener("click", () => {
      cancelRunner().catch((e) => alert(e.message));
    });
    el("missionQueueForm")?.addEventListener("submit", submitQueueDialog);
  }

  async function init() {
    loadStore();
    if (window.SoundsApp?.refreshSounds) {
      try {
        await window.SoundsApp.refreshSounds();
      } catch {
        /* ignore */
      }
    }
    await loadIoModuleCatalog();
    try {
      const maps = await fetch("/api/maps", { credentials: "include" });
      if (maps.ok) {
        const data = await maps.json();
        const list = Array.isArray(data.maps) ? data.maps : [];
        missionPositionCatalog = buildPositionCatalogFromMaps(list);
        syncPositionFieldDefs();
        const ids = list.map((m) => m.id).filter(Boolean);
        VARIABLE_FIELD_DEFS.switch_map[0].options = ids;
      }
    } catch {
      /* ignore */
    }
    await loadStoreFromBackend();
    bindEvents();
    renderMissionList();
  }

  window.MissionsApp = {
    init,
    getMissions: () => [...store.missions],
    getGroups: () => allGroups(),
    getMissionById: findMission,
    queueMission: openQueueDialog,
    enqueueMission,
    pauseRunner,
    continueRunner,
    cancelRunner,
    refreshQueue,
    clearQueue,
    getQueueSnapshot,
    renderQueueInto,
    onQueueUpdate(fn) {
      queueListeners.add(fn);
      return () => queueListeners.delete(fn);
    },
    startQueuePoll,
    stopQueuePoll,
    onPageShow() {
      void loadIoModuleCatalog().then(() => {
        if (!missionEditorViewEl?.hidden) renderMissionEditor();
        else {
          renderMissionList();
          startQueuePoll();
        }
      });
    },
    onPageHide() {
      stopQueuePoll();
    },
  };

  function boot() {
    init();
  }
  function onLocaleChange() {
    if (!missionEditorViewEl?.hidden) renderMissionEditor();
    else {
      renderMissionList();
      renderQueuePanel();
    }
    renderActionPalette();
  }
  window.addEventListener("lm:locale-change", onLocaleChange);

  window.addEventListener("lm:io-modules-changed", () => {
    void loadIoModuleCatalog().then(() => {
      if (!missionEditorViewEl?.hidden) renderMissionEditor();
    });
  });

  if (window.AuthApp?.isReady()) boot();
  else window.addEventListener("lm:auth-ready", boot, { once: true });
  window.addEventListener("lm:auth-logout", stopQueuePollForce);
})();
