// /AIWK_EXT4/popup.js
// 용도: EXT4 접속창 UI, client/token/profile 저장, WSS/NR/BUS 제어, compact 디버그 복사 관리.
// AIWK EXT4 v157-heart-title-sticky-common
const DEFAULTS = {
  wsUrl: "wss://n.yjm.kr/aiwk/ws",
  clientId: "",
  role: "A",
  group: "home",
  project: "IG_CRM",
  roomId: "",
  authToken: "",
  authMode: "empty",
  authRole: "guest",
  authUserType: "GUEST",
  authPayload: {}
};
const STORE_CONFIG = "AIWK_CONFIG";
const STORE_CLIENT_HISTORY = "AIWK_CLIENT_ID_HISTORY";
const STORE_CLIENT_PROFILE_VAULT = "AIWK_CLIENT_PROFILE_VAULT";
const STORE_ROUTE_PROFILE_INDEX = "AIWK_ROUTE_PROFILE_INDEX";
const STORE_TAB_CONFIG_PREFIX = "AIWK_ROUTE_CONFIG:";
const STORE_POPUP_FALLBACK_TAB_ID = "AIWK_POPUP_FALLBACK_TAB_ID";
const STORE_PANEL_RECT = "AIWK_PANEL_RECT";
const STORE_PANEL_MIN = "AIWK_PANEL_MIN";
const STORE_LAYOUT_EDIT = "AIWK_LAYOUT_EDIT";
const STORE_YY_DOCK = "AIWK_YY_DOCK_LAYOUT";
const STORE_AUTH_STATE = "AIWK_AUTH_STATE";
const STORE_VERSION = "AIWK_EXT4_VERSION";
const STORE_PROFILE_INSTANCE_ID = "AIWK_PROFILE_INSTANCE_ID";
const STORE_LEGACY_KEYS_REMOVED = "AIWK_LEGACY_KEYS_REMOVED";
const STORE_PROFILE_V152_INDEX = "aiwk/v152/profile_index";
const STORE_GLOBAL_USE_ENABLED = "aiwk/v152/runtime/use_enabled";
const STORE_DEBUG_LAST_REGISTER = "aiwk/v152/debug/last_register";
const LEGACY_PROFILE_KEYS = ["aiwk_ext4_profile", "aiwk_ext4_auth_token", "aiwk_ext4_auth_payload", "AIWK_EXT4_RESULT_LOG"];
const YYY_AIWK_URL_BASE = "https://n.yjm.kr/y/yyy_aiwk.php?path=01_basic&picker=&debug=";
let toastTimer = null;
let lastAuthBlockToastKey = "";
const AIWK_HEART_TOKEN_RE = /^(?:❤\ufe0f?|♥\ufe0f?|♡\ufe0f?|❣\ufe0f?|💛|💚|💙|💜|🧡|🖤|🤍|🤎|💖|💗|💓|💞|💕|💘|💝|💟)/u;
const AIWK_STATUS_TOKEN_GROUP = "(?:🟢|🟡|🔴|⚫|♻\ufe0f?|⚠\ufe0f?|⏹\ufe0f?)";
const AIWK_STATUS_PREFIX_PATTERN = "(?:" + AIWK_STATUS_TOKEN_GROUP + "\\s*){1,8}";
const AIWK_TITLE_PREFIX_WITH_COUNTERS_RE = new RegExp("^\\s*" + AIWK_STATUS_PREFIX_PATTERN + "[A-Z][A-Z0-9_-]{0,31}\\s+R:\\d{1,8}\\s+T:\\d{1,8}\\s*[-–]\\s*", "iu");
const AIWK_TITLE_PREFIX_SIMPLE_RE = new RegExp("^\\s*" + AIWK_STATUS_PREFIX_PATTERN + "[A-Z][A-Z0-9_-]{0,31}\\s*[-–]\\s*", "iu");
const AIWK_TITLE_TAIL_WITH_COUNTERS_RE = new RegExp("\\s*[-–]\\s*" + AIWK_STATUS_PREFIX_PATTERN + "[A-Z][A-Z0-9_-]{0,31}\\s+R:\\d{1,8}\\s+T:\\d{1,8}.*$", "iu");
function aiwkReadLeadingHeartTriple(title) {
  const src = String(title || "");
  const leading = (src.match(/^\s*/) || [""])[0];
  let idx = leading.length;
  let prefix = leading;
  for (let i = 0; i < 3; i++) {
    const m = src.slice(idx).match(AIWK_HEART_TOKEN_RE);
    if (!m) return { prefix: "", rest: src };
    prefix += m[0];
    idx += m[0].length;
  }
  return { prefix: prefix.trim(), rest: src.slice(idx).replace(/^\s+/, "") };
}
function aiwkJoinHeartPrefix(prefix, title) {
  const body = String(title || "").trim();
  const head = String(prefix || "").trim();
  if (!head) return body;
  return body ? head + " " + body : head;
}
function aiwkCleanStatusTitleBody(title, hasPreservedHeart) {
  let out = String(title || "").trim();
  for (let i = 0; i < 12; i++) {
    const before = out;
    out = out
      .replace(AIWK_TITLE_PREFIX_WITH_COUNTERS_RE, "")
      .replace(AIWK_TITLE_PREFIX_SIMPLE_RE, "")
      .replace(AIWK_TITLE_TAIL_WITH_COUNTERS_RE, "")
      .trim();
    const nestedHeart = aiwkReadLeadingHeartTriple(out);
    if (nestedHeart.prefix) {
      const rest = String(nestedHeart.rest || "").trim();
      if (hasPreservedHeart || AIWK_TITLE_PREFIX_WITH_COUNTERS_RE.test(rest) || AIWK_TITLE_PREFIX_SIMPLE_RE.test(rest)) {
        out = rest;
      }
    }
    if (out === before) break;
  }
  return out.trim();
}
function aiwkCleanStatusTitle(title) {
  if (window.AIWK_TITLE && typeof window.AIWK_TITLE.strip === "function") return window.AIWK_TITLE.strip(title);
  const heart = aiwkReadLeadingHeartTriple(title);
  const out = aiwkCleanStatusTitleBody(heart.prefix ? heart.rest : String(title || ""), !!heart.prefix);
  return aiwkJoinHeartPrefix(heart.prefix, out);
}
function aiwkStorageSafeErrorText(e) { return e && e.message ? e.message : String(e || ""); }
async function aiwkSafeStorageSet(obj) {
  try { await chrome.storage.local.set(obj); return { ok:1, via:"chrome.storage.local" }; }
  catch (e) {
    console.warn("[AIWK] chrome.storage.local set failed", aiwkStorageSafeErrorText(e));
    return { ok:0, error:aiwkStorageSafeErrorText(e) };
  }
}
function showToast(text, isError) {
  const el = document.getElementById("aiwkToast");
  if (!el) return;
  clearTimeout(toastTimer);
  const msg = String(text || "프로필 저장 완료");
  el.classList.toggle("err", !!isError);
  el.classList.add("on");
  el.title = isError ? "오류입니다. 내용을 복사한 뒤 X를 눌러 닫습니다." : "클릭하면 닫습니다.";
  el.innerHTML = "";
  const close = document.createElement("button");
  close.type = "button";
  close.className = "toast-x";
  close.textContent = "×";
  close.title = "닫기";
  close.addEventListener("click", function(ev){ ev.stopPropagation(); el.classList.remove("on"); });
  const body = document.createElement("div");
  body.className = "toast-body";
  body.textContent = msg;
  el.appendChild(close);
  el.appendChild(body);
  el.onclick = function(){ if (!isError) el.classList.remove("on"); };
  if (!isError) toastTimer = setTimeout(() => el.classList.remove("on"), 3500);
}
function setInlineNotice(text, level) {
  const el = document.getElementById("aiwkInlineNotice");
  if (!el) return;
  el.textContent = String(text || "");
  el.className = "aiwk-inline-notice " + (level || "info");
  el.hidden = !String(text || "").trim();
}
function isPreferredClientId(v) {
  // v126: client_id는 C1/U1처럼 운영 중 새로 만든 ID도 저장 목록에 반드시 보여야 한다.
  // 형식 검증만 통과하면 profile/history 후보로 인정한다.
  v = String(v || "").trim();
  return isValidClientId(v);
}
function clientIdLabel(v) {
  v = String(v || "").trim();
  return v.length > 24 ? v.slice(0, 10) + "…" + v.slice(-6) : v;
}

function statusInfoFromComm(comm, authState) {
  comm = comm || {};
  const wss = comm.wss || {};
  const bus = comm.tabbus || {};
  const nr = comm.nodered || {};
  const duplicate = !!wss.duplicate_route || /same_route_client_reconnected|replaced/i.test(String(wss.last_error || wss.last_close_reason || ""));
  const e1 = duplicate ? "♻️" : (wss.open ? "🟢" : (wss.connecting || wss.auto_reconnect ? "🟡" : "🔴"));
  const e2 = bus.registered ? "🟢" : (bus.ok ? "🟡" : "🔴");
  const e3 = nr.ok ? "🟢" : (nr.auto_keep ? "🟡" : "🔴");
  const triple = e1 + e2 + e3;
  if (duplicate) return { emoji:triple, text:"WSS/BUS/NR", cls:"replaced" };
  if (wss.open && bus.registered && nr.ok) return { emoji:triple, text:"WSS/BUS/NR", cls:"online" };
  if (wss.open || wss.connecting || wss.auto_reconnect || bus.registered || bus.ok || nr.ok || nr.auto_keep || (authState && (authState.authenticated || authState.connected))) return { emoji:triple, text:"WSS/BUS/NR", cls:"reconnecting" };
  return { emoji:triple, text:"WSS/BUS/NR", cls:"offline" };
}
function updatePopupTitleBadge(info) {
  const cidEl = document.getElementById("popupClientBadge");
  const stEl = document.getElementById("popupConnBadge");
  const cfg = (typeof getForm === "function") ? getForm() : {};
  const cid = String(cfg.clientId || "?").trim().toUpperCase() || "?";
  info = info || { emoji:"🔴", text:"offline", cls:"offline" };
  if (cidEl) cidEl.textContent = cid;
  if (stEl) {
    stEl.className = "popup-conn-badge " + (info.cls || "offline");
    stEl.textContent = (info.emoji || "🔴");
  }
}
function updatePopupTabBadge(tabObj) {
  const el = document.getElementById("popupTabBadge");
  if (!el) return;
  const id = tabObj && tabObj.id ? String(tabObj.id) : "-";
  el.textContent = "🧷 tab id: " + id;
  el.title = (tabObj && tabObj.title ? ("현재 tab_id: " + id + " / " + tabObj.title) : ("현재 tab_id: " + id)) + "\n클릭하면 Relay 창을 엽니다.";
  el.onclick = async function () {
    try {
      const res = await chrome.runtime.sendMessage({ type:"AIWK_OPEN_RELAY_PANEL", source:"popup_tab_badge", target_tab_id:id });
      showToast("Relay 창 열기: " + (res && res.ok ? "OK" : ((res && res.code) || "확인 필요")));
    } catch(e) {
      showToast("Relay 창 열기 실패: " + (e && e.message ? e.message : String(e)), true);
    }
  };
}
function stripUrlMd5(value) {
  if (Array.isArray(value)) return value.map(stripUrlMd5);
  if (!value || typeof value !== "object") return value;
  const out = {};
  Object.keys(value).forEach(k => {
    if (k === "url_md5") return;
    out[k] = stripUrlMd5(value[k]);
  });
  return out;
}
async function getProfileInstanceId() {
  const obj = await chrome.storage.local.get([STORE_PROFILE_INSTANCE_ID]);
  let id = String((obj && obj[STORE_PROFILE_INSTANCE_ID]) || "").trim();
  if (!id) {
    id = "pf_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 10);
    await chrome.storage.local.set({ [STORE_PROFILE_INSTANCE_ID]: id });
  }
  return id;
}

async function migratePopupStorageOnce() {
  const pairs = [
    ["AIWK_CONFIG_V1", STORE_CONFIG],
    ["AIWK_CLIENT_ID_HISTORY_V1", STORE_CLIENT_HISTORY],
    ["AIWK_CLIENT_PROFILE_VAULT_V042", STORE_CLIENT_PROFILE_VAULT],
    ["AIWK_POPUP_FALLBACK_TAB_ID_V1", STORE_POPUP_FALLBACK_TAB_ID],
    ["AIWK_PANEL_RECT_V1", STORE_PANEL_RECT],
    ["AIWK_PANEL_MIN_V1", STORE_PANEL_MIN],
    ["AIWK_LAYOUT_EDIT_V1", STORE_LAYOUT_EDIT],
    ["AIWK_YY_DOCK_LAYOUT_V1", STORE_YY_DOCK]
  ];
  const legacy = pairs.map(p => p[0]);
  const current = pairs.map(p => p[1]);
  const saved = await chrome.storage.local.get(legacy.concat(current));
  const setData = {};
  pairs.forEach(([oldKey, newKey]) => {
    if ((saved[newKey] === undefined || saved[newKey] === null || saved[newKey] === "") && saved[oldKey] !== undefined) {
      setData[newKey] = saved[oldKey];
    }
  });
  if (Object.keys(setData).length) await chrome.storage.local.set(setData);
  await chrome.storage.local.set({ [STORE_VERSION]: "v157-heart-title-sticky-common" });
  return { ok:1, migrated:Object.keys(setData).length };
}


function nsSafeSeg(v) {
  return encodeURIComponent(String(v || "").trim() || "-").replace(/%2F/gi, "_");
}
function profileV152Key(cfg, routeKey) {
  cfg = cfg || {};
  const group = String(cfg.group || "").trim();
  const project = String(cfg.project || "").trim();
  const room = String(cfg.roomId || cfg.room_id || "").trim();
  const cid = cleanClientId(cfg.clientId || cfg.client_id || "");
  const rk = String(routeKey || cfg.route_key || cfg.url5 || "").trim();
  return "aiwk/v152/profile/" + [group, project, room, cid, rk].map(nsSafeSeg).join("/");
}
function isAtomicProfileValid(cfg, routeKey) {
  cfg = cfg || {};
  const group = String(cfg.group || "").trim();
  const project = String(cfg.project || "").trim();
  const room = String(cfg.roomId || cfg.room_id || "").trim();
  const cid = cleanClientId(cfg.clientId || cfg.client_id || "");
  const token = String(cfg.authToken || cfg.token || "").trim();
  const rk = String(routeKey || cfg.route_key || cfg.url5 || "").trim();
  if (!group || !project || !room || !cid || !rk) return { ok:0, reason:"required_field_missing" };
  if (!isValidClientId(cid)) return { ok:0, reason:"client_id_invalid" };
  if (project === room) return { ok:0, reason:"project_room_mixed" };
  if (/^R[_-]/i.test(project)) return { ok:0, reason:"room_id_in_project" };
  const p = cfg.authPayload || {};
  if (p && typeof p === "object" && token) {
    const tg = String(p.group || "").trim();
    const tp = String(p.project || "").trim();
    const tr = String(p.room_id || p.roomId || "").trim();
    const tc = cleanClientId(p.client_id || p.clientId || "");
    if (tg && tg !== group) return { ok:0, reason:"token_group_mismatch", expected:tg, got:group };
    if (tp && tp !== project) return { ok:0, reason:"token_project_mismatch", expected:tp, got:project };
    if (tr && tr !== room) return { ok:0, reason:"token_room_mismatch", expected:tr, got:room };
    if (tc && tc !== cid) return { ok:0, reason:"token_client_mismatch", expected:tc, got:cid };
  }
  return { ok:1 };
}
async function setGlobalUseEnabled(flag, reason) {
  await chrome.storage.local.set({ [STORE_GLOBAL_USE_ENABLED]: !!flag });
  try { await sendToActiveTabMessage({ type:"AIWK_ROUTE_USE_SET", enabled:!!flag, reason:reason || (!!flag ? "global_use_on" : "global_use_off") }); } catch(e) {}
  if (!flag) {
    try { await sendToActiveTabMessage({ type:"AIWK_STOP_ALL_LOOPS", reason:reason || "global_use_off" }); } catch(e) {}
    try { await sendCommControl("wss_keep", { checked:false, skip_save:true }); } catch(e) {}
    try { await sendCommControl("bus_keep", { checked:false, skip_save:true }); } catch(e) {}
    try { await sendCommControl("nr_keep", { checked:false, skip_save:true }); } catch(e) {}
  }
  return { ok:1, enabled:!!flag };
}
async function saveProfileV152(cfg, tabObj, source) {
  cfg = cfg || {};
  if (!tabObj || !tabObj.id) return { ok:0, reason:"tab_missing" };
  const routeKey = routeKeyFromUrl(tabObj.url || "");
  const valid = isAtomicProfileValid(cfg, routeKey);
  if (!valid.ok) throw new Error("AIWK_PROFILE_V152_INVALID: " + valid.reason + (valid.expected ? " expected=" + valid.expected + " got=" + valid.got : ""));
  const token = String(cfg.authToken || cfg.token || "").trim();
  const key = profileV152Key(cfg, routeKey);
  const saved = await chrome.storage.local.get([STORE_PROFILE_V152_INDEX]);
  const index = saved[STORE_PROFILE_V152_INDEX] && typeof saved[STORE_PROFILE_V152_INDEX] === "object" ? saved[STORE_PROFILE_V152_INDEX] : {};
  const prev = index[key] || {};
  const tabs = Object.assign({}, prev.tabs || {});
  tabs[String(tabObj.id)] = { tab_id:String(tabObj.id), title:tabObj.title || "", url:tabObj.url || "", window_id:tabObj.windowId || "", last_seen:Date.now() };
  index[key] = {
    schema:"aiwk/v152/profile", key, clientId:cleanClientId(cfg.clientId), client_id:cleanClientId(cfg.clientId),
    group:String(cfg.group || "").trim(), project:String(cfg.project || "").trim(), roomId:String(cfg.roomId || "").trim(), room_id:String(cfg.roomId || "").trim(),
    route_key:routeKey, url5:routeKey, url_key:routeKey, wsUrl:String(cfg.wsUrl || DEFAULTS.wsUrl), role:normalizeRole(cfg.role || "A"),
    authToken:token, token:token, token_saved:token ? 1 : 0, authMode:String(cfg.authMode || "empty"), authRole:String(cfg.authRole || "guest"),
    authUserType:String(cfg.authUserType || ""), authPayload:cfg.authPayload || {}, tabs, last_tab_id:String(tabObj.id), last_title:tabObj.title || "", last_url:tabObj.url || "",
    source:source || "profile_save", updated_at:Date.now()
  };
  const keys = Object.keys(index).sort((a,b)=>Number(index[b].updated_at||0)-Number(index[a].updated_at||0)).slice(0,120);
  const compact = {}; keys.forEach(k=>compact[k]=index[k]);
  await chrome.storage.local.set({ [STORE_PROFILE_V152_INDEX]: compact });
  return { ok:1, key, route_key:routeKey, token_present:!!token };
}
async function getProfileV152ForTab(tabObj) {
  if (!tabObj || !tabObj.id) return null;
  const routeKey = routeKeyFromUrl(tabObj.url || "");
  const saved = await chrome.storage.local.get([STORE_PROFILE_V152_INDEX]);
  const index = saved[STORE_PROFILE_V152_INDEX] || {};
  const rows = Object.keys(index).map(k=>index[k]).filter(x => x && String(x.route_key || x.url5 || "") === routeKey && x.tabs && x.tabs[String(tabObj.id)]);
  rows.sort((a,b)=>Number(b.updated_at||0)-Number(a.updated_at||0));
  return rows[0] || null;
}

async function getAuthState() {
  const saved = await chrome.storage.local.get([STORE_AUTH_STATE]);
  return saved[STORE_AUTH_STATE] || { authenticated:false, connected:false };
}

async function setAuthState(patch) {
  const old = await getAuthState();
  const next = Object.assign({}, old, patch || {}, { updated_at: Date.now(), version: "v157-heart-title-sticky-common" });
  await chrome.storage.local.set({ [STORE_AUTH_STATE]: next });
  updateAuthButtons(next);
  return next;
}

function updateAuthButtons(state) {
  state = state || {};
  const openBtn = document.getElementById("openPanelBtn");
  const closeBtn = document.getElementById("closeAllBtn");
  const saveBtn = document.getElementById("saveBtn");
  if (openBtn) {
    openBtn.textContent = state.authenticated || state.connected ? "📡인증됨/재접속" : "📡WSS 인증등록";
    openBtn.title = state.authenticated || state.connected ? "이미 인증/접속된 상태입니다. 다시 누르면 현재 설정으로 WSS 소켓 연결 후 token register를 갱신합니다." : "현재 탭에 설정을 적용한 뒤 WSS 소켓 연결 → token register 순서로 인증등록합니다";
  }
  if (closeBtn) {
    closeBtn.disabled = !(state.authenticated || state.connected);
    closeBtn.title = state.authenticated || state.connected ? "인증/접속을 해제하고 현재 탭 AIWK 창을 닫습니다" : "아직 인증/접속 상태가 아닙니다";
  }
  if (saveBtn) saveBtn.title = "현재 설정을 저장합니다. 인증/접속 여부와 무관하게 사용 가능합니다.";
  updatePopupTitleBadge(statusInfoFromComm(null, state));
}

async function markLoggedOut(reason) {
  await setAuthState({ authenticated:false, connected:false, reason:reason || "logout" });
  setUserClosedBadge();
  setCommControlsEnabled(false);
}

async function scanProfileCleanupCandidates() {
  const tab = await activeTab();
  const currentTabId = tab && tab.id ? String(tab.id) : "";
  const saved = await chrome.storage.local.get([STORE_CLIENT_HISTORY, STORE_CLIENT_PROFILE_VAULT]);
  const history = normalizeClientHistory(saved[STORE_CLIENT_HISTORY] || []);
  const vault = normalizeClientProfileVault(saved[STORE_CLIENT_PROFILE_VAULT] || {});
  const byTab = {};
  history.forEach(x => { const tid=String(x.tab_id||""); if (!tid) return; byTab[tid]=byTab[tid]||new Set(); byTab[tid].add(cleanClientId(x.clientId||"")); });
  Object.keys(vault).forEach(k => { const x=vault[k]||{}; const tid=String(x.last_tab_id||""); if (!tid) return; byTab[tid]=byTab[tid]||new Set(); byTab[tid].add(cleanClientId(x.clientId||"")); });
  const conflicts = Object.keys(byTab).filter(tid => Array.from(byTab[tid]).filter(Boolean).length > 1).map(tid => ({ tab_id:tid, clients:Array.from(byTab[tid]).filter(Boolean) }));
  return { ok:1, current_tab_id:currentTabId, history_count:history.length, profile_count:Object.keys(vault).length, conflict_count:conflicts.length, conflicts:conflicts.slice(0,10) };
}

async function cleanupStaleTabConfigs() {
  const tabs = await chrome.tabs.query({});
  const live = new Set((tabs || []).map(t => String(t.id)));
  const all = await chrome.storage.local.get(null);
  const setData = {};
  let removedTabEntries = 0;
  Object.keys(all || {}).forEach(k => {
    if (k.indexOf(STORE_TAB_CONFIG_PREFIX) !== 0) return;
    const row = all[k] || {};
    if (!row.tabs || typeof row.tabs !== "object") return;
    const nextTabs = Object.assign({}, row.tabs || {});
    let changed = false;
    Object.keys(nextTabs).forEach(tid => {
      if (!tid || tid.indexOf("popup-") === 0) return;
      if (!live.has(String(tid))) {
        delete nextTabs[tid];
        removedTabEntries += 1;
        changed = true;
      }
    });
    if (changed) setData[k] = Object.assign({}, row, { tabs:nextTabs, updated_at:new Date().toISOString() });
  });
  if (Object.keys(setData).length) await chrome.storage.local.set(setData);
  const saved = await chrome.storage.local.get([STORE_CLIENT_HISTORY]);
  const before = normalizeClientHistory(saved[STORE_CLIENT_HISTORY] || []);
  const after = before.filter(item => {
    const tid = String(item.tab_id || "");
    if (!tid || tid.indexOf("popup-") === 0) return true;
    return live.has(tid);
  });
  await chrome.storage.local.set({ [STORE_CLIENT_HISTORY]: after });
  await refreshClientHistoryUI(await activeTab());
  return { ok:1, removed_tab_config:removedTabEntries, before_history:before.length, history_count:after.length };
}

function markClientIdError(flag, msg) {
  const el = document.getElementById("clientId");
  if (el) el.classList.toggle("aiwk-input-error", !!flag);
  if (flag && msg) showToast(msg, true);
}
function aiwkRoomTokenPrefix(cfg) {
  cfg = cfg || getForm();
  const room = String(cfg.roomId || "").trim();
  const token = String(cfg.authToken || "").trim();
  if (!room) return "";
  return room + (token ? ":" + token : "") + "@";
}
function aiwkTargetUri(clientId, endpointName, includeToken) {
  const cfg = getForm();
  const client = String(clientId || "N1").trim() || "N1";
  const ep = String(endpointName || "node-red").replace(/^#/, "") || "node-red";
  const room = String(cfg.roomId || "").trim();
  const token = includeToken ? String(cfg.authToken || "").trim() : "";
  const auth = room ? (room + (token ? ":" + token : "") + "@") : "";
  return "aiwk://" + auth + (cfg.group || "home") + "." + (cfg.project || "IG_CRM") + "." + client + "#" + ep;
}
function aiwkDefaultChatTarget(kind) {
  const cfg = getForm();
  if (kind === "self") return aiwkTargetUri(cfg.clientId || "C1", "ext4", false);
  return aiwkTargetUri("N1", "node-red", false);
}
function ensureChatTarget() {
  const to = document.getElementById("aiwkChatTo");
  if (to && !String(to.value || "").trim()) to.value = aiwkDefaultChatTarget("n1");
}

function isValidClientId(v) {
  v = String(v || "").trim();
  if (!v) return false;
  if (v.indexOf("AIWK1.") === 0) return false;
  if (v.length > 32) return false;
  return /^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(v);
}
function cleanClientId(v) {
  v = String(v || "").trim();
  return isValidClientId(v) ? v : "";
}
function normalizeRole(v) { v = String(v || "A").trim().toUpperCase(); return v === "B" ? "B" : "A"; }
function normalizeGroup(v) { return String(v || DEFAULTS.group).trim() || DEFAULTS.group; }
function normalizeProjectPair(groupValue, projectValue) {
  let group = normalizeGroup(groupValue);
  let project = String(projectValue || DEFAULTS.project).trim() || DEFAULTS.project;
  if (project.includes("/")) {
    const parts = project.split("/").map(v => String(v || "").trim()).filter(Boolean);
    if (parts.length >= 2) { group = parts[0]; project = parts.slice(1).join("_") || DEFAULTS.project; }
  }
  return { group, project };
}
function fallbackHash5(text) {
  let h = 0;
  text = String(text || "");
  for (let i = 0; i < text.length; i += 1) h = ((h << 5) - h + text.charCodeAt(i)) | 0;
  return ("00000" + (h >>> 0).toString(16)).slice(-5);
}
function routeKeyFromUrl(url) {
  try {
    if (window.AIWK && AIWK.urlContext && typeof AIWK.urlContext.makeContext === "function") {
      const ctx = AIWK.urlContext.makeContext(String(url || ""));
      return String(ctx.route_key || ctx.url_key || "").slice(0, 32);
    }
  } catch (e) {}
  return fallbackHash5(String(url || ""));
}
function tabConfigKey(routeKey) { return STORE_TAB_CONFIG_PREFIX + String(routeKey || "route"); }
function tabRouteConfigKey(tab) { return tabConfigKey(routeKeyFromUrl(tab && tab.url || "")); }
function isUsablePageTab(tab) {
  const url = String(tab && tab.url || "");
  if (!tab || !tab.id) return false;
  if (!url) return false;
  if (/^(chrome|edge|about|devtools|chrome-extension):/i.test(url)) return false;
  return true;
}
async function activeTab() {
  try {
    const wins = await chrome.windows.getAll({ populate:true, windowTypes:["normal"] });
    const focused = (wins || []).find(w => w.focused);
    const activeInFocused = focused && (focused.tabs || []).find(t => t.active && isUsablePageTab(t));
    if (activeInFocused) return activeInFocused;
    const last = await chrome.windows.getLastFocused({ populate:true, windowTypes:["normal"] }).catch(() => null);
    const activeLast = last && (last.tabs || []).find(t => t.active && isUsablePageTab(t));
    if (activeLast) return activeLast;
    const allActive = [];
    (wins || []).forEach(w => (w.tabs || []).forEach(t => { if (t.active && isUsablePageTab(t)) allActive.push(t); }));
    if (allActive[0]) return allActive[0];
    const any = [];
    (wins || []).forEach(w => (w.tabs || []).forEach(t => { if (isUsablePageTab(t)) any.push(t); }));
    if (any[0]) return any[0];
  } catch (e) {}
  const saved = await chrome.storage.local.get([STORE_POPUP_FALLBACK_TAB_ID]);
  let id = String(saved[STORE_POPUP_FALLBACK_TAB_ID] || "").trim();
  if (!id) {
    id = "popup-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8);
    await chrome.storage.local.set({ [STORE_POPUP_FALLBACK_TAB_ID]: id });
  }
  return { id, title:"popup fallback tab_id", url:"popup://aiwk-ext4" };
}
function globalConfigOnly(cfg) {
  cfg = cfg || {};
  // v152: 전역 설정에는 token/room/authPayload를 저장하지 않는다. 실제 인증 정본은 aiwk/v152/profile/... 만 사용한다.
  return { wsUrl:String(cfg.wsUrl || DEFAULTS.wsUrl).trim() || DEFAULTS.wsUrl, role:normalizeRole(cfg.role || DEFAULTS.role), updated_at:Date.now(), schema:"aiwk/v152/global_ui" };
}
function tabConfigOnly(cfg) {
  cfg = cfg || {};
  const pair = normalizeProjectPair(cfg.group, cfg.project);
  return {
    clientId:String(cfg.clientId || "").trim(),
    authToken:String(cfg.authToken || cfg.token || "").trim(),
    token:String(cfg.authToken || cfg.token || "").trim(),
    wsUrl:String(cfg.wsUrl || DEFAULTS.wsUrl),
    role:normalizeRole(cfg.role || "A"),
    group:pair.group,
    project:pair.project,
    roomId:String(cfg.roomId || "").trim(),
    authMode:String(cfg.authMode || "empty"),
    authRole:String(cfg.authRole || "guest"),
    authUserType:String(cfg.authUserType || (cfg.authPayload && cfg.authPayload.user_type) || ""),
    authPayload:cfg.authPayload || {}
  };
}
function isEndedTabConfig(cfg) {
  cfg = cfg || {};
  return !!cfg.ended || String(cfg.status || "").toUpperCase() === "END" || String(cfg.aiwk_status || "").toUpperCase() === "END";
}
function activeTabEndLabel(tab) {
  const id = tab && tab.id ? String(tab.id) : "";
  return id ? (id + "END") : ("popupEND");
}
async function ensureCurrentTabNewConfig(tab) {
  if (!tab || !tab.id) return { ok:0, reason:"tab_missing" };
  const key = tabRouteConfigKey(tab);
  const obj = await chrome.storage.local.get([key]);
  const routeKey = routeKeyFromUrl(tab.url || "");
  if (obj && obj[key]) {
    const cfgOld = obj[key] || {};
    const tabs = Object.assign({}, cfgOld.tabs || {});
    const tid = String(tab.id);
    if (!tabs[tid]) {
      tabs[tid] = {
        tab_id:tid,
        enabled:true,
        status:String(cfgOld.clientId || cfgOld.client_id || "").trim() ? "ACTIVE" : "NEW",
        aiwk_status:String(cfgOld.clientId || cfgOld.client_id || "").trim() ? "ACTIVE" : "NEW",
        ended:false,
        title:tab.title || "",
        url:tab.url || "",
        window_id:tab.windowId || "",
        updated_at:new Date().toISOString()
      };
      const cfgNext = Object.assign({}, cfgOld, {
        tab_id:tid,
        route_key:cfgOld.route_key || routeKey,
        url_key:cfgOld.url_key || routeKey,
        title:tab.title || cfgOld.title || "",
        url:tab.url || cfgOld.url || "",
        tabs:tabs,
        ended:false,
        disabled:false,
        updated_at:new Date().toISOString()
      });
      await chrome.storage.local.set({ [key]: cfgNext });
      return { ok:1, created:0, tab_created:1, key:key, config:cfgNext };
    }
    return { ok:1, created:0, key:key, config:cfgOld };
  }
  const cfg = {
    tab_id:String(tab.id),
    route_key:routeKey,
    url_key:routeKey,
    title:tab.title || "",
    url:tab.url || "",
    status:"NEW",
    aiwk_status:"NEW",
    assigned:false,
    ended:false,
    created_at:new Date().toISOString(),
    updated_at:new Date().toISOString(),
    source:"popup.first_run"
  };
  cfg.tabs = Object.assign({}, cfg.tabs || {});
  cfg.tabs[String(tab.id)] = {
    tab_id:String(tab.id),
    enabled:true,
    status:"NEW",
    title:tab.title || "",
    url:tab.url || "",
    window_id:tab.windowId || "",
    updated_at:new Date().toISOString()
  };
  await chrome.storage.local.set({ [key]: cfg });
  return { ok:1, created:1, key:key, config:cfg };
}
async function markCurrentTabEnded(reason) {
  const tab = await activeTab();
  if (!tab || !tab.id) return { ok:0, reason:"tab_missing" };
  const key = tabRouteConfigKey(tab);
  const old = await chrome.storage.local.get([key]);
  const oldCfg = old && old[key] ? old[key] : {};
  const cfgNow = getForm();
  const tabs = Object.assign({}, oldCfg.tabs || {});
  delete tabs[String(tab.id)];
  const endCfg = Object.assign({}, oldCfg, tabConfigOnly(cfgNow), {
    tab_id:String(tab.id),
    title:tab.title || oldCfg.title || "",
    url:tab.url || oldCfg.url || "",
    tabs:tabs,
    status:Object.keys(tabs).length ? "ACTIVE" : "NEW",
    aiwk_status:Object.keys(tabs).length ? "ACTIVE" : "NEW",
    ended:false,
    disabled:false,
    last_removed_tab_id:String(tab.id),
    last_removed_reason:String(reason || "route_use_off"),
    last_removed_at:new Date().toISOString(),
    updated_at:new Date().toISOString()
  });
  await chrome.storage.local.set({ [key]: endCfg });
  return { ok:1, tab_id:String(tab.id), key:key, removed:1 };
}
async function setCurrentRouteUse(flag) {
  const tab = await activeTab();
  if (flag) {
    const cfg = await saveConfig(getForm());
    try { await sendToActiveTabMessage({ type:"AIWK_ROUTE_USE_SET", enabled:true, reason:"route_use_on" }); } catch(e) {}
    await chrome.storage.local.set({ [STORE_GLOBAL_USE_ENABLED]: true });
    setInlineNotice("현재 url5 사용 ON: " + routeKeyFromUrl(tab && tab.url || ""), "info");
    return { ok:1, enabled:1, config:cfg };
  }
  try { await sendToActiveTabMessage({ type:"AIWK_ROUTE_USE_SET", enabled:false, reason:"route_use_off" }); } catch(e) {}
  const endRes = await markCurrentTabEnded("route_use_off");
  try { await sendToActiveTabMessage({ type:"AIWK_STOP_ALL_LOOPS", reason:"route_use_off" }); } catch(e) {}
  try { await sendToActiveTabMessage({ type:"AIWK_FRAME_DISPOSE_ALL", reason:"route_use_off" }); } catch(e) {}
  setCommControlsEnabled(false);
  await chrome.storage.local.set({ [STORE_GLOBAL_USE_ENABLED]: false });
  setInlineNotice("현재 url5 사용 OFF: AIWK 전체 런타임을 중단했습니다. WSS/BUS/NR/title/floating 루프가 정지됩니다.", "warn");
  return Object.assign({ enabled:0 }, endRes || {});
}
function getForm() {
  return { wsUrl:document.getElementById("wsUrl").value.trim() || DEFAULTS.wsUrl, clientId:document.getElementById("clientId").value.trim(), role:normalizeRole(document.getElementById("role").value), group:document.getElementById("group").value.trim() || DEFAULTS.group, project:document.getElementById("project").value.trim() || DEFAULTS.project, roomId:document.getElementById("roomId").value.trim(), authToken:document.getElementById("authToken").value.trim() };
}
function clampRetrySec(v) {
  return Math.max(2, Math.min(3600, parseInt(v || 0, 10) || 10));
}
function clampActionDelaySec(v) {
  return Math.max(1, Math.min(3600, parseInt(v || 0, 10) || 2));
}
function retrySecValue() {
  const w = document.getElementById("wssRetrySec");
  const n = document.getElementById("nrRetrySec");
  return clampRetrySec((w && w.value) || (n && n.value) || 10);
}
function busSecValue() {
  const b = document.getElementById("busRetrySec");
  return clampRetrySec((b && b.value) || 10);
}
function syncRetrySecInputs(sec) {
  sec = clampRetrySec(sec);
  const w = document.getElementById("wssRetrySec");
  const n = document.getElementById("nrRetrySec");
  if (w && String(w.value || "") !== String(sec)) w.value = String(sec);
  if (n && String(n.value || "") !== String(sec)) n.value = String(sec);
  return sec;
}
function syncBusSecInput(sec) {
  sec = clampRetrySec(sec);
  const b = document.getElementById("busRetrySec");
  if (b && String(b.value || "") !== String(sec)) b.value = String(sec);
  return sec;
}
function defaultDelaySettings() {
  return { base:10, wss_nr:10, bus:10, action:2, source:"default" };
}
function normalizeDelaySettings(raw) {
  raw = raw || {};
  return {
    base:clampRetrySec(raw.base || raw.common_sec || raw.module_default_sec || 10),
    wss_nr:clampRetrySec(raw.wss_nr || raw.retry || raw.wss_sec || raw.nr_sec || raw.common_sec || raw.base || 10),
    bus:clampRetrySec(raw.bus || raw.bus_sec || raw.common_sec || raw.base || 10),
    action:clampActionDelaySec(raw.action || raw.action_default_sec || 2),
    modules:raw.modules && typeof raw.modules === "object" ? raw.modules : {},
    actions:raw.actions && typeof raw.actions === "object" ? raw.actions : {},
    source:raw.source || "normalized",
    updated_at:raw.updated_at || ""
  };
}
function readDelaySettingsForm() {
  const base = defaultDelaySettings();
  return {
    base:clampRetrySec((document.getElementById("delayBaseSec") || {}).value || base.base),
    wss_nr:clampRetrySec((document.getElementById("delayWssNrSec") || {}).value || base.wss_nr),
    bus:clampRetrySec((document.getElementById("delayBusSec") || {}).value || base.bus),
    action:clampActionDelaySec((document.getElementById("delayActionSec") || {}).value || base.action),
    source:"popup.delay_settings",
    updated_at:new Date().toISOString()
  };
}
function renderDelaySettingsForm(delays, meta) {
  delays = normalizeDelaySettings(Object.assign(defaultDelaySettings(), delays || {}));
  const pairs = [
    ["delayBaseSec", delays.base],
    ["delayWssNrSec", delays.wss_nr],
    ["delayBusSec", delays.bus],
    ["delayActionSec", delays.action]
  ];
  pairs.forEach(function (row) {
    const el = document.getElementById(row[0]);
    if (el) el.value = String(row[1]);
  });
  const info = document.getElementById("delaySettingsInfo");
  if (info) info.textContent = "url5:" + ((meta && meta.route_key) || "-") + " / key:" + ((meta && meta.key) || "-") + " / 기본:" + delays.base + "초 / WSS/NR:" + delays.wss_nr + "초 / BUS:" + delays.bus + "초 / 액션:" + delays.action + "초";
}
async function loadCurrentDelaySettings() {
  const tab = await activeTab();
  const key = tabRouteConfigKey(tab);
  await ensureCurrentTabNewConfig(tab).catch(()=>{});
  const saved = await chrome.storage.local.get([key]);
  const cfg = saved && saved[key] ? saved[key] : {};
  const delays = normalizeDelaySettings(cfg.delays || {});
  renderDelaySettingsForm(delays, { key:key, route_key:routeKeyFromUrl(tab && tab.url || "") });
  return { ok:1, key:key, route_key:routeKeyFromUrl(tab && tab.url || ""), delays:delays };
}
async function saveCurrentDelaySettings() {
  const tab = await activeTab();
  const ensured = await ensureCurrentTabNewConfig(tab).catch(()=>null);
  const key = tabRouteConfigKey(tab);
  const saved = await chrome.storage.local.get([key]);
  const cfg = Object.assign({}, ensured && ensured.config || {}, saved && saved[key] || {});
  const delays = readDelaySettingsForm();
  const next = Object.assign({}, cfg, {
    route_key:cfg.route_key || routeKeyFromUrl(tab && tab.url || ""),
    url_key:cfg.url_key || routeKeyFromUrl(tab && tab.url || ""),
    delays:delays,
    updated_at:new Date().toISOString()
  });
  await chrome.storage.local.set({ [key]: next });
  renderDelaySettingsForm(delays, { key:key, route_key:next.route_key });
  return { ok:1, key:key, route_key:next.route_key, delays:delays };
}
function applyDelaySettingsToCommInputs(delays) {
  delays = normalizeDelaySettings(delays || readDelaySettingsForm());
  const sec = clampRetrySec(delays.wss_nr || delays.base || 10);
  syncRetrySecInputs(sec);
  syncBusSecInput(delays.bus || delays.base || 10);
  return sec;
}
function setForm(cfg) {
  const v = Object.assign({}, DEFAULTS, cfg || {});
  document.getElementById("wsUrl").value = v.wsUrl || DEFAULTS.wsUrl;
  document.getElementById("clientId").value = v.clientId || "";
  document.getElementById("role").value = normalizeRole(v.role || DEFAULTS.role);
  document.getElementById("group").value = v.group || DEFAULTS.group;
  document.getElementById("project").value = v.project || DEFAULTS.project;
  document.getElementById("roomId").value = v.roomId || "";
  document.getElementById("authToken").value = v.authToken || "";
}
function isHexToken(v) { return /^[a-f0-9]{64}$/i.test(String(v || "").trim()); }
function b64urlDecode(s) { const safe = String(s || "").replace(/-/g,"+").replace(/_/g,"/"); const pad = safe.length % 4 ? "=".repeat(4 - (safe.length % 4)) : ""; return atob(safe + pad); }
function roleFromPerm(v) { const raw = String(v || "").trim(); const low = raw.toLowerCase(); if (low === "guest") return "guest"; if (low === "editor") return "editor"; if (low === "dev") return "dev"; const up = raw.toUpperCase(); if (up === "USER") return "editor"; if (up === "HOST") return "host"; if (up === "ADMIN") return "dev"; return ""; }
function userTypeFromPayload(payload, fallbackRole) {
  payload = payload || {};
  const raw = String(payload.user_type || payload.userType || payload.perm || payload.role || fallbackRole || "").trim();
  const up = raw.toUpperCase();
  if (up === "ADMIN" || up === "DEV") return "AIWK1_ADMIN";
  if (up === "HOST") return "AIWK1_HOST"; if (up === "USER" || up === "EDITOR") return "AIWK1_USER";
  if (fallbackRole === "dev") return "AIWK1_ADMIN";
  if (fallbackRole === "editor") return "AIWK1_USER";
  return "GUEST";
}
function displayRole(role, userType) {
  if (role === "dev") return "개발자" + (userType ? " / " + userType : "");
  if (role === "editor") return "편집자" + (userType ? " / " + userType : "");
  if (role === "host") return "특수 계정" + (userType ? " / " + userType : "");
  return "손님";
}
function parseLocalToken(token) {
  token = String(token || "").trim();
  if (!token) return { ok:1, authRole:"guest", authMode:"empty", authPayload:{ user_type:"GUEST", role:"guest" }, authUserType:"GUEST", message:"guest" };
  if (token.toLowerCase() === "guest") return { ok:0, error:"guest는 토큰으로 저장하지 않습니다. 손님 접속은 token을 비우십시오." };
  if (token.indexOf("AIWK1.") === 0) {
    const parts = token.split(".");
    if (parts.length !== 3) return { ok:0, error:"AIWK1 토큰 형식이 틀렸습니다." };
    try { const payload = JSON.parse(b64urlDecode(parts[1])); const role = roleFromPerm(payload.role || payload.perm || ""); if (!role) return { ok:0, error:"AIWK1 토큰에 유효한 role/perm이 없습니다." }; const userType = userTypeFromPayload(payload, role); payload.user_type = payload.user_type || userType; return { ok:1, authRole:role, authMode:"AIWK1", authPayload:payload, authUserType:userType, message:"AIWK1 " + displayRole(role, userType) }; } catch(e) { return { ok:0, error:"AIWK1 토큰 해석 실패: " + e.message }; }
  }
  if (token[0] === "{") {
    try { const payload = JSON.parse(token); const role = roleFromPerm(payload.role || payload.perm || ""); if (!role) return { ok:0, error:"JSON 토큰에 유효한 role/perm이 없습니다." }; const userType = userTypeFromPayload(payload, role); payload.user_type = payload.user_type || userType; return { ok:1, authRole:role, authMode:"json", authPayload:payload, authUserType:userType, message:"JSON " + displayRole(role, userType) }; } catch(e) { return { ok:0, error:"JSON 토큰 해석 실패: " + e.message }; }
  }
  return { ok:0, error:"토큰이 틀렸습니다. 비우면 guest, 서버 토큰은 64자리 hex, 또는 JSON/AIWK1 토큰을 입력하십시오." };
}
function verifyRemoteToken(token) {
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage({ type:"AIWK_AUTH_VERIFY_REMOTE_TOKEN", token }, res => {
      const le = chrome.runtime.lastError;
      if (le) { reject(new Error(le.message)); return; }
      if (!res || !res.ok) { reject(new Error((res && (res.error || res.code)) || "서버 토큰 검증 실패")); return; }
      resolve(res);
    });
  });
}
async function validateLogin(cfg) {
  const token = String(cfg.authToken || "").trim();
  if (!String(cfg.clientId || "").trim()) { markClientIdError(true, "client_id 미입력: 직접 입력하거나 저장된 client_id/token 목록에서 선택하십시오."); throw new Error("client_id를 입력하거나 저장된 탭별 client_id에서 선택하십시오."); }
  if (!isValidClientId(cfg.clientId)) { markClientIdError(true, "client_id 형식 오류: C1, A1, CHAT1, WEB1, N1처럼 영문으로 시작해야 합니다."); throw new Error("client_id 형식 오류: 영문 시작 + 영문/숫자/_/- 만 허용됩니다."); }
  markClientIdError(false);
  if (!token) return Object.assign({}, cfg, { authToken:"", authMode:"empty", authRole:"guest", authUserType:"GUEST", authPayload:{ user_type:"GUEST", role:"guest" } });
  if (isHexToken(token)) {
    const res = await verifyRemoteToken(token);
    return Object.assign({}, cfg, { authMode:"remote-token", authRole:"dev", authUserType:"AIWK1_ADMIN", authPayload:{ role:"dev", perm:"ADMIN", user_type:"AIWK1_ADMIN", source:"popup-remote-token", token_len:token.length, url:res.url || "" } });
  }
  const parsed = parseLocalToken(token);
  if (!parsed.ok) throw new Error(parsed.error);
  return Object.assign({}, cfg, parsed);
}
function normalizeClientHistory(list) {
  if (!Array.isArray(list)) return [];
  const map = new Map();
  list.forEach(item => {
    if (!item) return;
    const clientId = cleanClientId(item.clientId);
    if (!clientId || !isPreferredClientId(clientId)) return;
    const authToken = String(item.authToken || item.token || "").trim();
    const group = String(item.group || "home").trim() || "home";
    const project = String(item.project || "IG_CRM").trim() || "IG_CRM";
    const routeKey = String(item.route_key || item.url_key || item.url5 || "").trim();
    const tabId = String(item.tab_id || item.last_tab_id || "").trim();
    const key = [routeKey || "no-url5", tabId || "no-tab", group, project, clientId, tokenKey(authToken)].join("|");
    const row = Object.assign({}, item, { clientId, authToken, token: authToken, token_saved: authToken ? 1 : 0, group, project, route_key:routeKey, url_key:routeKey, url5:routeKey, authPayload: item.authPayload || {} });
    const old = map.get(key);
    if (!old || Number(row.updated_at || 0) >= Number(old.updated_at || 0)) map.set(key, row);
  });
  return Array.from(map.values()).sort((a,b)=>Number(b.updated_at||0)-Number(a.updated_at||0)).slice(0, 40);
}
function shortTitle(v) {
  v = String(v || "").trim();
  return v.length > 32 ? v.slice(0, 32) + "…" : v;
}
function tokenKey(v) {
  const s = String(v || "").trim();
  if (!s) return "empty";
  return String(s.length) + ":" + s.slice(0, 8) + ":" + s.slice(-6);
}
function profileVaultKey(cfg) {
  cfg = cfg || {};
  return [String(cfg.group || "home"), String(cfg.project || "IG_CRM"), String(cfg.roomId || ""), String(cfg.clientId || ""), tokenKey(cfg.authToken || cfg.token || "")].join("|");
}
function normalizeClientProfileVault(obj) {
  const out = {};
  if (!obj || typeof obj !== "object") return out;
  Object.keys(obj).forEach(k => {
    const item = obj[k] || {};
    const clientId = cleanClientId(item.clientId);
    if (!clientId || !isPreferredClientId(clientId)) return;
    const authToken = String(item.authToken || item.token || "").trim();
    const row = Object.assign({}, item, { clientId, authToken, token: authToken, token_saved: authToken ? 1 : 0, authPayload: item.authPayload || {} });
    out[profileVaultKey(row)] = row;
  });
  return out;
}
async function getClientProfileVault() {
  const saved = await chrome.storage.local.get([STORE_CLIENT_PROFILE_VAULT]);
  return normalizeClientProfileVault(saved[STORE_CLIENT_PROFILE_VAULT] || {});
}

function normalizeRouteProfileIndex(obj) {
  const out = {};
  if (!obj || typeof obj !== "object") return out;
  Object.keys(obj).forEach(function (rk) {
    const routeKey = String(rk || "").trim();
    if (!routeKey) return;
    const item = obj[rk] || {};
    const clientId = cleanClientId(item.clientId || item.client_id || "");
    const authToken = String(item.authToken || item.token || "").trim();
    const tabs = item.tabs && typeof item.tabs === "object" ? item.tabs : {};
    if (!clientId && !authToken && !Object.keys(tabs).length) return;
    out[routeKey] = Object.assign({}, item, {
      route_key: routeKey,
      url_key: routeKey,
      url5: routeKey,
      clientId: clientId,
      client_id: clientId,
      authToken: authToken,
      token: authToken,
      token_saved: authToken ? 1 : 0,
      tabs: tabs,
      updated_at: Number(item.updated_at || 0) || 0
    });
  });
  return out;
}
async function getRouteProfileIndex() {
  const saved = await chrome.storage.local.get([STORE_ROUTE_PROFILE_INDEX]);
  return normalizeRouteProfileIndex(saved[STORE_ROUTE_PROFILE_INDEX] || {});
}
async function updateRouteProfileIndex(cfg, tabObj) {
  cfg = cfg || {};
  if (!tabObj || !tabObj.id) return await getRouteProfileIndex();
  const routeKey = routeKeyFromUrl(tabObj.url || "");
  const clientId = cleanClientId(cfg.clientId || "");
  const index = await getRouteProfileIndex();
  const prev = index[routeKey] || {};
  const tabs = Object.assign({}, prev.tabs || {});
  tabs[String(tabObj.id)] = {
    tab_id:String(tabObj.id),
    enabled:true,
    status:"ACTIVE",
    title:tabObj.title || "",
    url:tabObj.url || "",
    window_id:tabObj.windowId || "",
    updated_at:Date.now()
  };
  const authToken = String(cfg.authToken || cfg.token || "").trim();
  index[routeKey] = Object.assign({}, prev, {
    route_key:routeKey,
    url_key:routeKey,
    url5:routeKey,
    clientId:clientId || cleanClientId(prev.clientId || ""),
    client_id:clientId || cleanClientId(prev.client_id || ""),
    authToken:authToken,
    token:authToken,
    token_saved:authToken ? 1 : 0,
    group:String(cfg.group || prev.group || "home"),
    project:String(cfg.project || prev.project || "IG_CRM"),
    roomId:String(cfg.roomId || prev.roomId || ""),
    role:normalizeRole(cfg.role || prev.role || "A"),
    wsUrl:String(cfg.wsUrl || prev.wsUrl || DEFAULTS.wsUrl),
    authMode:String(cfg.authMode || prev.authMode || "empty"),
    authRole:String(cfg.authRole || prev.authRole || "guest"),
    authUserType:String(cfg.authUserType || prev.authUserType || ""),
    authPayload:cfg.authPayload || prev.authPayload || {},
    last_tab_id:String(tabObj.id),
    last_title:tabObj.title || "",
    last_url:tabObj.url || "",
    tabs:tabs,
    updated_at:Date.now()
  });
  const keys = Object.keys(index).sort(function(a,b){ return Number(index[b].updated_at||0)-Number(index[a].updated_at||0); }).slice(0, 80);
  const compact = {};
  keys.forEach(function(k){ compact[k] = index[k]; });
  await chrome.storage.local.set({ [STORE_ROUTE_PROFILE_INDEX]: compact });
  return compact;
}
function routeProfileForTab(index, tabObj) {
  if (!tabObj || !tabObj.id) return null;
  const routeKey = routeKeyFromUrl(tabObj.url || "");
  const row = index && index[routeKey] ? index[routeKey] : null;
  if (!row || isEndedTabConfig(row)) return null;
  return row;
}
async function updateClientProfileVault(cfg, tabObj) {
  cfg = cfg || {};
  const clientId = cleanClientId(cfg.clientId);
  if (!clientId || !isPreferredClientId(clientId)) return await getClientProfileVault();
  const vault = await getClientProfileVault();
  const authToken = String(cfg.authToken || cfg.token || "").trim();
  const row = {
    clientId,
    authToken,
    token: authToken,
    token_saved: authToken ? 1 : 0,
    group: String(cfg.group || "home"),
    project: String(cfg.project || "IG_CRM"),
    roomId: String(cfg.roomId || ""),
    role: normalizeRole(cfg.role || "A"),
    wsUrl: String(cfg.wsUrl || DEFAULTS.wsUrl),
    authMode: String(cfg.authMode || "empty"),
    authRole: String(cfg.authRole || "guest"),
    authUserType: String(cfg.authUserType || ""),
    authPayload: cfg.authPayload || {},
    route_key: tabObj && tabObj.url ? routeKeyFromUrl(tabObj.url || "") : "",
    url_key: tabObj && tabObj.url ? routeKeyFromUrl(tabObj.url || "") : "",
    url5: tabObj && tabObj.url ? routeKeyFromUrl(tabObj.url || "") : "",
    last_tab_id: tabObj && tabObj.id ? String(tabObj.id) : "",
    last_title: tabObj && tabObj.title ? String(tabObj.title) : "",
    last_url: tabObj && tabObj.url ? String(tabObj.url) : "",
    updated_at: Date.now()
  };
  vault[profileVaultKey(row)] = row;
  await chrome.storage.local.set({ [STORE_CLIENT_PROFILE_VAULT]: vault });
  return vault;
}
async function deleteClientProfileVaultItem(key) {
  const vault = await getClientProfileVault();
  delete vault[String(key || "")];
  await chrome.storage.local.set({ [STORE_CLIENT_PROFILE_VAULT]: vault });
  return vault;
}

function renderClientHistory(history, activeTabObj, vault) {
  const sel = document.getElementById("clientIdHistory");
  const info = document.getElementById("tabInfo");
  if (!sel) return;
  const currentId = activeTabObj && activeTabObj.id ? String(activeTabObj.id) : "";
  history = normalizeClientHistory(history || []);
  vault = normalizeClientProfileVault(vault || {});
  sel.innerHTML = "";
  const base = document.createElement("option");
  base.value = "";
  const total = history.length + Object.keys(vault).length;
  base.textContent = total ? "저장된 client_id/token 선택" : "저장된 client_id 없음";
  sel.appendChild(base);
  Object.keys(vault).sort((a,b) => (vault[b].updated_at||0)-(vault[a].updated_at||0)).forEach(key => {
    const item = vault[key] || {};
    const opt = document.createElement("option");
    opt.value = String(item.clientId || "");
    opt.dataset.kind = "profile";
    opt.dataset.profileKey = key;
    opt.dataset.tabId = String(item.last_tab_id || "");
    opt.dataset.url = String(item.last_url || "");
    opt.dataset.authToken = String(item.authToken || item.token || "");
    opt.dataset.authMode = String(item.authMode || "empty");
    opt.dataset.authRole = String(item.authRole || "guest");
    opt.dataset.authUserType = String(item.authUserType || "");
    opt.dataset.authPayload = JSON.stringify(item.authPayload || {});
    opt.dataset.group = String(item.group || "home");
    opt.dataset.project = String(item.project || "IG_CRM");
    opt.dataset.roomId = String(item.roomId || "");
    opt.dataset.wsUrl = String(item.wsUrl || DEFAULTS.wsUrl);
    opt.dataset.role = String(item.role || "A");
    const tokenMark = (item.authToken || item.token || item.token_saved) ? " / 🔐" : " / 🔓";
    opt.textContent = "프로필 · " + String(item.clientId || "") + tokenMark + " / " + String(item.group || "home") + "/" + String(item.project || "IG_CRM") + " / tab:" + String(item.last_tab_id || "-");
    sel.appendChild(opt);
  });
  (history || []).forEach(item => {
    const opt = document.createElement("option");
    opt.value = String(item.clientId || "");
    opt.dataset.kind = "tab";
    opt.dataset.tabId = String(item.tab_id || "");
    opt.dataset.url = String(item.url || "");
    opt.dataset.authToken = String(item.authToken || item.token || "");
    opt.dataset.authMode = String(item.authMode || "empty");
    opt.dataset.authRole = String(item.authRole || "guest");
    opt.dataset.authUserType = String(item.authUserType || "");
    opt.dataset.authPayload = JSON.stringify(item.authPayload || {});
    const mark = String(item.tab_id || "") === currentId ? "현재탭 · " : "탭 · ";
    const tokenMark = (item.authToken || item.token || item.token_saved) ? " / 🔐" : " / 🔓";
    opt.textContent = mark + String(item.clientId || "") + " / tab:" + String(item.tab_id || "?") + tokenMark + " / " + shortTitle(item.title || item.url || "");
    sel.appendChild(opt);
  });
  const list = document.getElementById("clientProfileList");
  if (list) {
    list.innerHTML = "";
    const merged = new Map();
    function mergeRow(kind, key, item, idx) {
      item = item || {};
      const cid = clientIdLabel(item.clientId || "");
      if (!cid) return;
      const tid = String(item.tab_id || item.last_tab_id || "");
      const group = String(item.group || "home");
      const project = String(item.project || "IG_CRM");
      const room = String(item.roomId || item.room || "");
      const mkey = [group, project, room, cid, tid || "profile"].join("|");
      const old = merged.get(mkey);
      const row = { kind, key, idx, item, cid, tid, updated_at:Number(item.updated_at || 0) || 0 };
      if (!old || row.kind === "tab" || row.updated_at > old.updated_at) merged.set(mkey, Object.assign({}, old || {}, row, { kinds:Array.from(new Set([].concat(old && old.kinds || [], [kind]))) }));
    }
    Object.keys(vault).sort((a,b) => (vault[b].updated_at||0)-(vault[a].updated_at||0)).forEach(key => mergeRow("profile", key, vault[key] || {}));
    (history || []).forEach((item, idx) => mergeRow("tab", String(item.tab_id || "") + "|" + String(item.clientId || ""), item || {}, idx));
    const rows = Array.from(merged.values()).sort((a,b) => (b.updated_at||0)-(a.updated_at||0));
    rows.slice(0, 20).forEach(row => {
      const item = row.item || {};
      const wrap = document.createElement("div");
      wrap.className = "client-profile-item";
      const main = document.createElement("div");
      main.className = "client-profile-main";
      const tokenMark = (item.authToken || item.token || item.token_saved) ? "🔐" : "🔓";
      const title = aiwkCleanStatusTitle(item.title || item.last_title || item.url || item.last_url || "");
      const kindMark = row.kinds && row.kinds.length > 1 ? "저장" : (row.kind === "profile" ? "프로필" : "탭");
      main.textContent = kindMark + " · " + clientIdLabel(item.clientId) + " / " + String(item.group || "home") + "." + String(item.project || "IG_CRM") + " / " + tokenMark + " / tab:" + String(item.last_tab_id || item.tab_id || "-") + (title ? " / " + title.slice(0, 60) : "");
      main.title = main.textContent + "\n내부 profile/tab 저장값을 병합해서 표시합니다. 클릭하면 현재 폼에 선택됩니다.";
      main.addEventListener("click", async () => {
        document.getElementById("clientId").value = String(item.clientId || "");
        if (item.wsUrl) document.getElementById("wsUrl").value = String(item.wsUrl || DEFAULTS.wsUrl);
        if (item.role) document.getElementById("role").value = normalizeRole(item.role);
        if (item.group) document.getElementById("group").value = String(item.group || "home");
        if (item.project) document.getElementById("project").value = String(item.project || "IG_CRM");
        if (item.roomId || item.room) document.getElementById("roomId").value = String(item.roomId || item.room || "");
        document.getElementById("authToken").value = String(item.authToken || item.token || "");
        const cfgNow = getForm();
        cfgNow.authMode = String(item.authMode || "empty");
        cfgNow.authRole = String(item.authRole || "guest");
        cfgNow.authUserType = String(item.authUserType || "");
        cfgNow.authPayload = item.authPayload || {};
        renderTokenInfo(cfgNow);
        ensureChatTarget();
        await saveConfig(cfgNow).catch(()=>{});
        showToast("선택: " + clientIdLabel(item.clientId) + " / " + tokenMark);
      });
      const del = document.createElement("button");
      del.type = "button";
      del.className = "client-profile-del";
      del.textContent = "×";
      del.title = "이 저장 항목 삭제";
      del.addEventListener("click", async (ev) => {
        ev.stopPropagation();
        try {
          if (row.kinds && row.kinds.indexOf("profile") >= 0 && row.key) await deleteClientProfileVaultItem(row.key);
          const saved = await chrome.storage.local.get([STORE_CLIENT_HISTORY]);
          let h = normalizeClientHistory(saved[STORE_CLIENT_HISTORY] || []);
          h = h.filter(x => !(String(x.tab_id || "") === String(item.tab_id || item.last_tab_id || "") && String(x.clientId || "") === String(item.clientId || "")));
          await chrome.storage.local.set({ [STORE_CLIENT_HISTORY]: h });
          const routeKey = item.route_key || item.url_key || "";
          if (routeKey) await chrome.storage.local.remove([tabConfigKey(routeKey)]);
          await refreshClientHistoryUI(await activeTab());
          showToast("저장 항목 삭제: " + clientIdLabel(item.clientId));
        } catch(e) { showToast("삭제 실패: " + e.message, true); }
      });
      wrap.appendChild(main);
      wrap.appendChild(del);
      list.appendChild(wrap);
    });
  }
  if (info) {
    info.textContent = activeTabObj && activeTabObj.id ? ("현재 tab_id: " + activeTabObj.id + " / " + shortTitle(activeTabObj.title || activeTabObj.url || "")) : "현재 tab_id 확인 불가";
  }
}
async function cleanupClientProfiles() {
  const closeBtn = document.getElementById("popupCloseBtn");
  if (closeBtn) closeBtn.addEventListener("click", () => { try { window.close(); } catch(e) {} });
  const tab = await activeTab();
  const currentTabId = tab && tab.id ? String(tab.id) : "";
  const cfgNow = getForm();
  const currentClient = cleanClientId(cfgNow.clientId || "");
  const currentGroup = String(cfgNow.group || "home").trim() || "home";
  const currentProject = String(cfgNow.project || "IG_CRM").trim() || "IG_CRM";
  const currentRoom = String(cfgNow.roomId || "").trim();
  const currentToken = String(cfgNow.authToken || cfgNow.token || "").trim();
  const saved = await chrome.storage.local.get([STORE_CLIENT_HISTORY, STORE_CLIENT_PROFILE_VAULT]);
  const history0 = normalizeClientHistory(saved[STORE_CLIENT_HISTORY] || []);
  const vault0 = normalizeClientProfileVault(saved[STORE_CLIENT_PROFILE_VAULT] || {});

  const bestHistory = new Map();
  history0.forEach(item => {
    const clientId = cleanClientId(item.clientId || "");
    if (!clientId || !isPreferredClientId(clientId)) return;
    const group = String(item.group || currentGroup || "home").trim() || "home";
    const project = String(item.project || currentProject || "IG_CRM").trim() || "IG_CRM";
    const key = [group, project, clientId].join("|");
    const cur = bestHistory.get(key);
    const isCurrent = currentTabId && String(item.tab_id || "") === currentTabId;
    const oldIsCurrent = cur && currentTabId && String(cur.tab_id || "") === currentTabId;
    if (!cur || (isCurrent && !oldIsCurrent) || Number(item.updated_at || 0) > Number(cur.updated_at || 0)) {
      bestHistory.set(key, item);
    }
  });
  let history = Array.from(bestHistory.values());
  if (currentTabId && currentClient) {
    history = history.filter(item => !(String(item.tab_id || "") === currentTabId && cleanClientId(item.clientId || "") === currentClient));
    history.unshift({
      tab_id: currentTabId,
      clientId: currentClient,
      authToken: currentToken,
      token: currentToken,
      token_saved: currentToken ? 1 : 0,
      group: currentGroup,
      project: currentProject,
      roomId: currentRoom,
      authMode: String(cfgNow.authMode || "empty"),
      authRole: String(cfgNow.authRole || ""),
      authUserType: String(cfgNow.authUserType || ""),
      authPayload: cfgNow.authPayload || {},
      title: tab && tab.title || "",
      url: tab && tab.url || "",
      updated_at: Date.now()
    });
  }
  history = normalizeClientHistory(history).slice(0, 20);

  const bestVault = new Map();
  Object.keys(vault0).forEach(k => {
    const item = vault0[k] || {};
    const clientId = cleanClientId(item.clientId || "");
    if (!clientId || !isPreferredClientId(clientId)) return;
    const group = String(item.group || currentGroup || "home").trim() || "home";
    const project = String(item.project || currentProject || "IG_CRM").trim() || "IG_CRM";
    const roomId = String(item.roomId || item.room || "").trim();
    const key = [group, project, roomId, clientId].join("|");
    const cur = bestVault.get(key);
    const isCurrent = currentClient && clientId === currentClient && group === currentGroup && project === currentProject && (!currentRoom || roomId === currentRoom);
    const oldIsCurrent = cur && currentClient && cleanClientId(cur.clientId || "") === currentClient;
    if (!cur || (isCurrent && !oldIsCurrent) || Number(item.updated_at || 0) > Number(cur.updated_at || 0)) {
      bestVault.set(key, item);
    }
  });
  let vault = {};
  Array.from(bestVault.values()).forEach(item => { vault[profileVaultKey(item)] = item; });
  if (currentClient) {
    const row = {
      clientId: currentClient,
      authToken: currentToken,
      token: currentToken,
      token_saved: currentToken ? 1 : 0,
      group: currentGroup,
      project: currentProject,
      roomId: currentRoom,
      role: normalizeRole(cfgNow.role || "A"),
      wsUrl: String(cfgNow.wsUrl || DEFAULTS.wsUrl),
      authMode: String(cfgNow.authMode || "empty"),
      authRole: String(cfgNow.authRole || "guest"),
      authUserType: String(cfgNow.authUserType || ""),
      authPayload: cfgNow.authPayload || {},
      last_tab_id: currentTabId,
      last_title: tab && tab.title || "",
      last_url: tab && tab.url || "",
      updated_at: Date.now()
    };
    vault[profileVaultKey(row)] = row;
  }
  vault = normalizeClientProfileVault(vault);
  await chrome.storage.local.set({ [STORE_CLIENT_HISTORY]: history, [STORE_CLIENT_PROFILE_VAULT]: vault });
  await refreshClientHistoryUI(tab);
  return { ok:1, before_history:history0.length, before_profile:Object.keys(vault0).length, history_count:history.length, profile_count:Object.keys(vault).length };
}
function historyRowsFromTabConfigs(all, fallbackCfg) {
  const rows = [];
  all = all || {};
  fallbackCfg = fallbackCfg || {};
  Object.keys(all).forEach(function (key) {
    if (key.indexOf(STORE_TAB_CONFIG_PREFIX) !== 0) return;
    const cfg = all[key] || {};
    if (isEndedTabConfig(cfg)) return;
    const clientId = cleanClientId(cfg.clientId || "");
    if (!clientId || !isPreferredClientId(clientId)) return;
    const tabId = String(cfg.tab_id || key.slice(STORE_TAB_CONFIG_PREFIX.length) || "");
    const authToken = String(cfg.authToken || cfg.token || fallbackCfg.authToken || fallbackCfg.token || "").trim();
    rows.push({
      tab_id: tabId,
      clientId: clientId,
      authToken: authToken,
      token: authToken,
      token_saved: authToken ? 1 : 0,
      group: String(cfg.group || fallbackCfg.group || "home"),
      project: String(cfg.project || fallbackCfg.project || "IG_CRM"),
      roomId: String(cfg.roomId || cfg.room || fallbackCfg.roomId || ""),
      role: normalizeRole(cfg.role || fallbackCfg.role || "A"),
      wsUrl: String(cfg.wsUrl || fallbackCfg.wsUrl || DEFAULTS.wsUrl),
      authMode: String(cfg.authMode || fallbackCfg.authMode || "empty"),
      authRole: String(cfg.authRole || fallbackCfg.authRole || "guest"),
      authUserType: String(cfg.authUserType || fallbackCfg.authUserType || ""),
      authPayload: cfg.authPayload || fallbackCfg.authPayload || {},
      title: cfg.title || cfg.last_title || "",
      url: cfg.url || cfg.last_url || "",
      updated_at: cfg.updated_at ? Date.parse(cfg.updated_at) || Date.now() : Date.now(),
      source: cfg.source || "tab_config"
    });
  });
  return rows;
}

async function refreshClientHistoryUI(tabObj) {
  const all = await chrome.storage.local.get(null);
  const fallbackCfg = getForm();
  let history = normalizeClientHistory([].concat(all[STORE_CLIENT_HISTORY] || [], historyRowsFromTabConfigs(all, fallbackCfg)));
  const currentTab = tabObj || await activeTab();
  const currentClient = cleanClientId(fallbackCfg.clientId || "");
  if (currentTab && currentTab.id && currentClient && isPreferredClientId(currentClient)) {
    const authToken = String(fallbackCfg.authToken || fallbackCfg.token || "").trim();
    history = normalizeClientHistory([].concat([{
      tab_id:String(currentTab.id), clientId:currentClient, authToken:authToken, token:authToken, token_saved:authToken ? 1 : 0,
      group:String(fallbackCfg.group || "home"), project:String(fallbackCfg.project || "IG_CRM"), roomId:String(fallbackCfg.roomId || ""),
      role:normalizeRole(fallbackCfg.role || "A"), wsUrl:String(fallbackCfg.wsUrl || DEFAULTS.wsUrl),
      authMode:String(fallbackCfg.authMode || "empty"), authRole:String(fallbackCfg.authRole || "guest"),
      authUserType:String(fallbackCfg.authUserType || ""), authPayload:fallbackCfg.authPayload || {},
      title:currentTab.title || "", url:currentTab.url || "", updated_at:Date.now(), source:"current_form"
    }], history));
  }
  const vault = await getClientProfileVault();
  renderClientHistory(history, currentTab, vault);
  return { history, vault };
}
async function updateClientHistory(cfg, tabObj) {
  const saved = await chrome.storage.local.get([STORE_CLIENT_HISTORY]);
  let history = normalizeClientHistory(saved[STORE_CLIENT_HISTORY] || []);
  if (!tabObj || !tabObj.id || !cfg || !isPreferredClientId(cfg.clientId)) return history;
  const routeKey = routeKeyFromUrl(tabObj.url || "");
  history = history.filter(item => !(String(item.route_key || item.url_key || item.url5 || "") === routeKey && String(item.tab_id || "") === String(tabObj.id)));
  const authToken = String(cfg.authToken || cfg.token || "").trim();
  history.unshift({
    route_key: routeKey,
    url_key: routeKey,
    url5: routeKey,
    tab_id: tabObj.id,
    clientId: String(cfg.clientId || "").trim(),
    authToken,
    token: authToken,
    token_saved: authToken ? 1 : 0,
    group:String(cfg.group || "home"),
    project:String(cfg.project || "IG_CRM"),
    roomId:String(cfg.roomId || ""),
    role:normalizeRole(cfg.role || "A"),
    wsUrl:String(cfg.wsUrl || DEFAULTS.wsUrl),
    authMode: String(cfg.authMode || "empty"),
    authRole: String(cfg.authRole || ""),
    authUserType: String(cfg.authUserType || ""),
    authPayload: cfg.authPayload || {},
    title: tabObj.title || "",
    url: tabObj.url || "",
    updated_at: Date.now()
  });
  history = normalizeClientHistory(history);
  await chrome.storage.local.set({ [STORE_CLIENT_HISTORY]: history });
  return history;
}
async function deleteSelectedClientHistory() {
  const sel = document.getElementById("clientIdHistory");
  if (!sel || sel.selectedIndex <= 0) throw new Error("삭제할 저장 항목을 먼저 선택하십시오.");
  const opt = sel.options[sel.selectedIndex];
  const kind = String(opt.dataset.kind || "tab");
  const tabId = String(opt.dataset.tabId || "");
  const clientId = cleanClientId(opt.value || "") || "unknown";
  if (kind === "profile") {
    await deleteClientProfileVaultItem(opt.dataset.profileKey || "");
    const closeBtn = document.getElementById("popupCloseBtn");
  if (closeBtn) closeBtn.addEventListener("click", () => { try { window.close(); } catch(e) {} });
  const tab = await activeTab();
    await refreshClientHistoryUI(tab);
    return { tabId: tabId || "profile", clientId, count: -1 };
  }
  const saved = await chrome.storage.local.get([STORE_CLIENT_HISTORY]);
  let history = normalizeClientHistory(saved[STORE_CLIENT_HISTORY] || []);
  const routeKey = (history.find(item => String(item.tab_id || "") === tabId && String(item.clientId || "") === clientId) || {}).route_key || "";
  history = history.filter(item => !(String(item.tab_id || "") === tabId && String(item.clientId || "") === clientId));
  const removeKeys = [];
  if (routeKey) removeKeys.push(tabConfigKey(routeKey));
  await chrome.storage.local.set({ [STORE_CLIENT_HISTORY]: history });
  if (removeKeys.length) await chrome.storage.local.remove(removeKeys);
  const closeBtn = document.getElementById("popupCloseBtn");
  if (closeBtn) closeBtn.addEventListener("click", () => { try { window.close(); } catch(e) {} });
  const tab = await activeTab();
  await refreshClientHistoryUI(tab);
  return { tabId, clientId, count: history.length };
}
async function cleanupStaleSameClientTabConfigs(cfg, currentTab) {
  cfg = cfg || getForm();
  const currentTabId = currentTab && currentTab.id ? String(currentTab.id) : "";
  const clientId = cleanClientId(cfg.clientId || "");
  if (!currentTabId || !clientId) return { ok:0, skipped:"missing_current_tab_or_client" };
  const tabs = await chrome.tabs.query({});
  const alive = new Set((tabs || []).map(t => String(t.id)));
  const all = await chrome.storage.local.get(null);
  const setData = {};
  let removedTabs = 0;
  Object.keys(all || {}).forEach(k => {
    if (k.indexOf(STORE_TAB_CONFIG_PREFIX) !== 0) return;
    const row = all[k] || {};
    const sameClient = cleanClientId(row.clientId || "") === clientId;
    if (!sameClient || !row.tabs || typeof row.tabs !== "object") return;
    const nextTabs = Object.assign({}, row.tabs || {});
    let changed = false;
    Object.keys(nextTabs).forEach(tid => {
      if (String(tid) === currentTabId) return;
      if (!alive.has(String(tid))) {
        delete nextTabs[tid];
        removedTabs += 1;
        changed = true;
      }
    });
    if (changed) setData[k] = Object.assign({}, row, { tabs:nextTabs, updated_at:new Date().toISOString() });
  });
  if (Object.keys(setData).length) await chrome.storage.local.set(setData);
  return { ok:1, removed:removedTabs, updated_keys:Object.keys(setData) };
}

async function saveConfig(cfg) {
  cfg = cfg || getForm();
  const pair = normalizeProjectPair(cfg.group, cfg.project);
  cfg = Object.assign({}, cfg, pair);
  setForm(cfg);
  const closeBtn = document.getElementById("popupCloseBtn");
  if (closeBtn) closeBtn.addEventListener("click", () => { try { window.close(); } catch(e) {} });
  const tab = await activeTab();
  const saveData = { [STORE_CONFIG]: globalConfigOnly(cfg) };
  if (tab && tab.id) {
    const profileInstanceId = await getProfileInstanceId();
    const routeKey = routeKeyFromUrl(tab.url || "");
    const routeKeyName = tabRouteConfigKey(tab);
    const oldObj = await chrome.storage.local.get([routeKeyName]).catch(()=>({}));
    const oldRouteCfg = oldObj && oldObj[routeKeyName] ? oldObj[routeKeyName] : {};
    const routeTabs = Object.assign({}, oldRouteCfg.tabs || {});
    routeTabs[String(tab.id)] = Object.assign({}, routeTabs[String(tab.id)] || {}, {
      tab_id:String(tab.id),
      enabled:true,
      status:"ACTIVE",
      aiwk_status:"ACTIVE",
      ended:false,
      title:tab.title || "",
      url:tab.url || "",
      window_id:tab.windowId || "",
      updated_at:new Date().toISOString()
    });
    saveData[routeKeyName] = Object.assign({}, oldRouteCfg, tabConfigOnly(cfg), {
      tab_id:String(tab.id),
      route_key:routeKey,
      url_key:routeKey,
      profile_instance_id:profileInstanceId,
      profileInstanceId:profileInstanceId,
      title:tab.title || "",
      url:tab.url || "",
      tabs:routeTabs,
      status:"ACTIVE",
      aiwk_status:"ACTIVE",
      ended:false,
      disabled:false,
      updated_at:new Date().toISOString()
    });
  }
  await chrome.storage.local.set(saveData);
  try { if (tab && tab.id) await saveProfileV152(cfg, tab, "saveConfig"); } catch(e) { throw e; }
  try { await cleanupStaleSameClientTabConfigs(cfg, tab); } catch(e) {}
  const history = await updateClientHistory(cfg, tab);
  await updateClientProfileVault(cfg, tab);
  await updateRouteProfileIndex(cfg, tab);
  renderTokenInfo(cfg);
  ensureChatTarget();
  setCommControlsEnabled(commControlsEnabled(cfg));
  await refreshClientHistoryUI(tab);
  scanAndRenderDomainModules().catch(e => { renderDomainInfo({ matched:0, url:{ hostname:"" }, message:e.message }, tab); });
  return cfg;
}


async function requireCurrentTabConfig(cfg, tab) {
  cfg = cfg || getForm();
  tab = tab || await activeTab();
  if (!tab || !tab.id) {
    throw new Error("AIWK_ACTIVE_TAB_MISSING: 현재 적용할 실제 웹 tab_id를 찾지 못했습니다.");
  }
  const key = tabRouteConfigKey(tab);
  const saved = await chrome.storage.local.get([key]);
  const row = saved && saved[key] ? saved[key] : null;
  if (!row) {
    throw new Error("AIWK_ROUTE_CONFIG_MISSING: 필요 키 없음: " + key + " / 해결: [프로필 저장]으로 현재 url5에 client_id/token을 먼저 저장하십시오.");
  }
  const want = cleanClientId(cfg.clientId || "");
  const got = cleanClientId(row.clientId || row.client_id || "");
  if (!want) {
    throw new Error("AIWK_CLIENT_ID_EMPTY: client_id가 비었습니다. C1, A1, CHAT1처럼 먼저 입력하십시오.");
  }
  if (!got) {
    throw new Error("AIWK_ROUTE_CONFIG_CLIENT_EMPTY: " + key + " 안에 client_id가 없습니다. 해결: [프로필 저장]으로 현재 url5에 client_id를 저장하십시오.");
  }
  if (got !== want) {
    throw new Error("AIWK_ROUTE_CONFIG_CLIENT_MISMATCH: 현재 url5=" + routeKeyFromUrl(tab.url || "") + " 저장 client=" + got + " / 폼 client=" + want + " / 해결: [프로필 저장]으로 현재 url5를 " + want + "로 덮어쓴 뒤 다시 WSS 인증등록하십시오.");
  }
  const tabState = row.tabs && row.tabs[String(tab.id)] ? row.tabs[String(tab.id)] : null;
  if (!tabState) {
    throw new Error("AIWK_ROUTE_TAB_DISABLED: 현재 tab_id=" + String(tab.id) + " 는 사용 OFF 상태입니다. 접속창 맨 위 [사용]을 체크한 뒤 다시 실행하십시오.");
  }
  const ended = isEndedTabConfig(row) || tabState.enabled === false || !!tabState.ended || String(tabState.status || "").toUpperCase() === "END";
  if (ended) {
    throw new Error("AIWK_ROUTE_CONFIG_ENDED: " + key + " 는 END 상태입니다. 해결: client_id/token을 다시 저장하여 ACTIVE로 전환하십시오.");
  }
  if (!String(row.profile_instance_id || row.profileInstanceId || "").trim()) {
    throw new Error("AIWK_PROFILE_INSTANCE_ID_MISSING: " + key + " 안에 profile_instance_id가 없습니다. 해결: [프로필 저장]으로 현재 url5 설정을 다시 저장하십시오.");
  }
  return { ok:1, tab:tab, tab_id:String(tab.id), key:key, config:row };
}


function renderTokenInfo(cfg) {
  const info = document.getElementById("tokenInfo");
  if (!info) return;
  cfg = cfg || getForm();
  const token = String(cfg.authToken || cfg.token || "").trim();
  const role = String(cfg.authRole || "").trim();
  const userType = String(cfg.authUserType || "").trim();
  info.textContent = token ? ("🔐" + (role ? " / " + role : "") + (userType ? " / " + userType : "")) : "🔓 / guest";
  info.title = token ? "토큰 있음 / Token available" : "토큰 없음 / Token missing";
  setCommControlsEnabled(commControlsEnabled(cfg));
}
function setConnected(on, status) {
  const box = document.getElementById("winControls");
  if (box) box.hidden = !on;
  document.body.classList.toggle("aiwk-connected", !!on);
  if (status === "USER_CLOSED" || status === "END") {
    updatePopupTitleBadge({ emoji:"⏹", text:"user closed", cls:"closed" });
    return;
  }
  updatePopupTitleBadge({ emoji:on ? "🟢🟢🟢" : "🔴🔴🔴", text:on ? "online" : "offline", cls:on ? "online" : "offline" });
}
function setUserClosedBadge() {
  const box = document.getElementById("winControls");
  if (box) box.hidden = true;
  document.body.classList.remove("aiwk-connected");
  updatePopupTitleBadge({ emoji:"⏹", text:"종료", cls:"closed" });
}
function applyPlaceholderTooltips(root) {
  root = root || document;
  root.querySelectorAll("input[placeholder], textarea[placeholder], select[placeholder]").forEach(el => {
    const ph = String(el.getAttribute("placeholder") || "").trim();
    if (ph && !String(el.getAttribute("title") || "").trim()) el.setAttribute("title", "placeholder: " + ph);
  });
}
function controlWindows(action) {
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage({ type:"AIWK_EXT4_CONTROL_FROM_POPUP", action }, res => {
      const le = chrome.runtime.lastError;
      if (le) { reject(new Error(le.message)); return; }
      if (!res || !res.ok) { reject(new Error((res && (res.error || res.code)) || "창 제어 실패")); return; }
      resolve(res);
    });
  });
}
async function bindControlButton(id, action, label) {
  const btn = document.getElementById(id);
  if (!btn) return;
  btn.addEventListener("click", async () => {
    try {
      const res = await controlWindows(action);
      showToast((label || action) + " 완료 / count=" + (res.count ?? ""));
      if (action === "close_all") setUserClosedBadge();
    } catch(e) { showToast((label || action) + " 실패: " + e.message, true); }
  });
}
function openMain(cfg) {
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage({ type:"AIWK_EXT4_OPEN_MAIN_FROM_POPUP", config:cfg }, res => {
      const le = chrome.runtime.lastError;
      if (le) { reject(new Error(le.message)); return; }
      if (!res || !res.ok) { reject(new Error((res && (res.error || res.code)) || "AIWK 창 열기 실패")); return; }
      resolve(res);
    });
  });
}

function openManualTest(cfg) {
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage({ type:"AIWK_EXT4_OPEN_MANUAL_TEST_FROM_POPUP", config:cfg }, res => {
      const le = chrome.runtime.lastError;
      if (le) { reject(new Error(le.message)); return; }
      if (!res || !res.ok) { reject(new Error((res && (res.error || res.code)) || "수동 테스트 창 열기 실패")); return; }
      resolve(res);
    });
  });
}

const STORE_DOMAIN_TAB_SETTINGS = "AIWK_DOMAIN_TAB_SETTINGS_V041";
let currentDomainScan = null;
let currentDomainSettings = null;
function extUrl(path) { return chrome.runtime.getURL(path); }
async function loadExtJson(path) {
  const res = await fetch(extUrl(path), { cache:"no-store" });
  if (!res.ok) throw new Error("JSON 로드 실패: " + path + " / " + res.status);
  return await res.json();
}
function parseTabUrl(tab) {
  try {
    const u = new URL((tab && tab.url) || "");
    const parts = u.pathname.split("/").map(v => String(v || "").trim()).filter(Boolean).map(cleanRoutePart).filter(Boolean);
    const port = u.port || "";
    return { href:u.href, protocol:u.protocol, host:u.hostname.toLowerCase(), hostname:u.hostname.toLowerCase(), port, host_folder:hostFolderForDomain(u.hostname, port, u.protocol), pathname:u.pathname, path_parts:parts, first_folder:parts[0] || "" };
  } catch(e) {
    return { href:(tab && tab.url) || "", protocol:"", host:"", hostname:"", port:"", host_folder:"", pathname:"", path_parts:[], first_folder:"" };
  }
}
function cleanRoutePart(v) {
  v = String(v == null ? "" : v).trim();
  try { v = decodeURIComponent(v); } catch(e) {}
  v = v.replace(/[\\]/g, "/").replace(/^\/+|\/+$/g, "");
  return v.replace(/[^A-Za-z0-9._\-가-힣]/g, "_");
}
function isDefaultPortForDomain(protocol, port) {
  protocol = String(protocol || "").toLowerCase();
  port = String(port || "");
  return !port || (protocol === "http:" && port === "80") || (protocol === "https:" && port === "443");
}
function hostFolderForDomain(hostname, port, protocol) {
  const host = cleanRoutePart(String(hostname || "").toLowerCase());
  if (!host) return "";
  if (!isDefaultPortForDomain(protocol, port)) return host + "_" + cleanRoutePart(port);
  return host;
}
function buildDomainCandidateLevelsFromParsed(parsed) {
  const rows = [];
  if (!parsed || !parsed.host_folder) return { parts:{ root_key:"", base_domain:"", host:"", host_folder:"" }, levels:rows };
  let cur = "AIWK_COMMON/domains/" + parsed.host_folder;
  rows.push({ level:"host", key:parsed.host_folder, base_path:cur });
  (parsed.path_parts || []).forEach((part, idx) => {
    cur += "/" + part;
    rows.push({ level:"path" + (idx + 1), key:parsed.host_folder + "/" + (parsed.path_parts || []).slice(0, idx + 1).join("/"), base_path:cur });
  });
  return { parts:{ root_key:parsed.host_folder, base_domain:parsed.host_folder, host:parsed.hostname || parsed.host || "", host_folder:parsed.host_folder }, levels:rows };
}
function buildDomainCandidateLevels(hostname) {
  const parsed = { hostname:String(hostname || "").toLowerCase(), host_folder:hostFolderForDomain(hostname, "", "https:"), path_parts:[] };
  return buildDomainCandidateLevelsFromParsed(parsed);
}
async function loadExtJsonMaybe(path) {
  try { return await loadExtJson(path); } catch(e) { return null; }
}
function deepMergeObj(target, source) {
  target = target && typeof target === "object" && !Array.isArray(target) ? target : {};
  source = source && typeof source === "object" && !Array.isArray(source) ? source : {};
  Object.keys(source).forEach(k => {
    const sv = source[k];
    const tv = target[k];
    if (sv && typeof sv === "object" && !Array.isArray(sv) && tv && typeof tv === "object" && !Array.isArray(tv)) target[k] = deepMergeObj(Object.assign({}, tv), sv);
    else target[k] = sv;
  });
  return target;
}
function domainModuleId(row, domainKey) {
  row = row || {};
  if (row.module_id) return String(row.module_id);
  return [domainKey || "domain", row.dev_key || "unknown", row.feature_key || row.module_key || "module"].join(".");
}
function normalizeDomainModules(modulesJson, level) {
  const rows = Array.isArray(modulesJson && modulesJson.modules) ? modulesJson.modules : [];
  return rows.map(m => {
    const out = Object.assign({}, m);
    out.module_id = domainModuleId(out, level.key || level.level);
    out.domain_level = level.level;
    out.domain_key = level.key || "domains";
    out.service_or_group = out.service_or_group || level.key || "domains";
    out.representative_domain = out.representative_domain || level.key || "";
    out.base_path = level.base_path;
    out.title_ko = out.title_ko || out.title || out.module_key || out.feature_key || out.module_id;
    out.install_required = out.install_required !== false;
    return out;
  });
}
async function scanDomainModulesForTab(tab) {
  const parsed = parseTabUrl(tab);
  const built = buildDomainCandidateLevelsFromParsed(parsed);
  const loaded = [];
  let mergedDomain = {};
  let mergedI18nKo = {};
  const moduleMap = new Map();
  for (const level of built.levels) {
    const index = await loadExtJsonMaybe(level.base_path + "/index.json");
    const domain = await loadExtJsonMaybe(level.base_path + "/domain.json");
    const modulesPath = level.base_path + "/" + ((index && index.module_index) || "modules.json");
    const modulesJson = await loadExtJsonMaybe(modulesPath);
    const i18nKo = await loadExtJsonMaybe(level.base_path + "/i18n/ko.json");
    if (!index && !domain && !modulesJson && !i18nKo) continue;
    const modules = normalizeDomainModules(modulesJson || { modules:[] }, level);
    loaded.push({ level:level.level, key:level.key, base_path:level.base_path, index_path:index ? level.base_path + "/index.json" : "", domain_path:domain ? level.base_path + "/domain.json" : "", modules_path:modulesJson ? modulesPath : "", i18n_ko_path:i18nKo ? level.base_path + "/i18n/ko.json" : "", module_count:modules.length });
    if (domain) mergedDomain = deepMergeObj(mergedDomain, domain);
    if (i18nKo) mergedI18nKo = deepMergeObj(mergedI18nKo, i18nKo);
    modules.forEach(m => moduleMap.set(m.module_id, m));
  }
  const modules = Array.from(moduleMap.values());
  const last = loaded.length ? loaded[loaded.length - 1] : null;
  return { ok:1, matched:loaded.length ? 1 : 0, url:parsed, domain_parts:built.parts, candidate_levels:built.levels, loaded_levels:loaded, service_or_group:last ? last.key : (built.parts.root_key || ""), representative_domain:last ? last.key : (built.parts.host || ""), base_path:last ? last.base_path : "", folder_first:parsed.first_folder, folder_route:parsed.path_parts.join("/"), domain_index:{}, merged_domain:mergedDomain, i18n_ko:mergedI18nKo, modules, modules_load_error:"", message:loaded.length ? "DOMAIN_FOLDER_INHERITANCE_LOADED" : "이 도메인에 존재하는 AIWK 도메인 폴더가 없습니다." };
}
async function getDomainSettingsAll() {
  const saved = await chrome.storage.local.get([STORE_DOMAIN_TAB_SETTINGS]);
  const all = saved[STORE_DOMAIN_TAB_SETTINGS];
  return all && typeof all === "object" ? all : {};
}
async function setDomainSettingsAll(all) { await chrome.storage.local.set({ [STORE_DOMAIN_TAB_SETTINGS]: all || {} }); return all || {}; }
function defaultDomainSettings(tab, scan) {
  return { tab_id:String(tab && tab.id || ""), title:tab && tab.title || "", url:tab && tab.url || "", representative_domain:scan && scan.representative_domain || "", service_or_group:scan && scan.service_or_group || "", folder_route:scan && scan.folder_route || "", folder_first:scan && scan.folder_first || "", modules:{}, updated_at:new Date().toISOString() };
}
function renderDomainInfo(scan, tab) {
  const el = document.getElementById("domainInfo");
  if (!el) return;
  if (!scan) { el.textContent = "도메인 검사 전"; return; }
  if (!scan.matched) { el.textContent = "현재 tab:" + ((tab && tab.id) || "?") + " / " + (scan.url && scan.url.hostname || "?") + " / 등록된 도메인 모듈 없음"; return; }
  const levels = (scan.loaded_levels || []).map(x => x.key).join(" → ");
  el.textContent = "tab:" + ((tab && tab.id) || "?") + " / 도메인상속:" + (levels || scan.representative_domain || "-") + " / folder:" + (scan.folder_route || "-") + " / 모듈:" + scan.modules.length + "개";
}

function moduleAppliesToCurrentDomain(module, scan) {
  if (!module || !scan || !scan.url) return false;
  const host = String(scan.url.hostname || scan.url.host || "").toLowerCase();
  const hosts = []
    .concat(module.hosts || [])
    .concat(module.host_aliases || [])
    .concat(module.match_hosts || []);
  if (hosts.length) {
    return hosts.map(x => String(x || "").toLowerCase()).some(h => h === host || (h.charAt(0) === "." && host.endsWith(h)));
  }
  const rep = String(module.representative_domain || module.full_domain || "").toLowerCase();
  if (rep && rep !== host && !host.endsWith("." + rep)) return false;
  return true;
}

function renderDomainModules(scan, settings) {
  const box = document.getElementById("domainModuleList");
  if (!box) return;
  box.innerHTML = "";
  if (!scan || !scan.matched || !scan.modules.length) { box.textContent = "현재 도메인에서 감지된 모듈 없음"; return; }
  settings = settings || { modules:{} };
  scan.modules.forEach(m => {
    const st = settings.modules && settings.modules[m.module_id] ? settings.modules[m.module_id] : {};
    const item = document.createElement("div"); item.className = "domain-module-item";
    const applies = moduleAppliesToCurrentDomain(m, scan);
    if (!applies) item.classList.add("aiwk-domain-module-disabled");
    const head = document.createElement("div"); head.className = "domain-module-head";
    head.innerHTML = '<span>' + (m.ui && m.ui.icon ? m.ui.icon + ' ' : '🧩 ') + escHtml(m.title_ko || m.title || m.module_key || m.module_id) + '</span><span>' + escHtml(m.dev_key || '') + '</span>';
    const meta = document.createElement("div"); meta.className = "domain-module-meta"; meta.textContent = m.module_id + " / " + (m.description_ko || m.description || "");
    const stateLine = document.createElement("div"); stateLine.className = "domain-module-state"; stateLine.textContent = applies ? "현재 도메인 사용 가능" : "현재 도메인 불일치: 설치/사용 불가";
    const actions = document.createElement("div"); actions.className = "domain-module-actions";
    const installLabel = document.createElement("label"); const install = document.createElement("input"); install.type = "checkbox"; install.checked = !!st.installed; install.disabled = !applies; install.dataset.moduleId = m.module_id; install.dataset.action = "installed"; installLabel.appendChild(install); installLabel.appendChild(document.createTextNode("설치"));
    const enableLabel = document.createElement("label"); const enable = document.createElement("input"); enable.type = "checkbox"; enable.checked = !!st.enabled; enable.disabled = !applies; enable.dataset.moduleId = m.module_id; enable.dataset.action = "enabled"; enableLabel.appendChild(enable); enableLabel.appendChild(document.createTextNode("사용"));
    const mode = document.createElement("select"); mode.disabled = !applies; mode.dataset.moduleId = m.module_id; mode.dataset.action = "run_mode"; ["manual","auto","test"].forEach(v => { const o = document.createElement("option"); o.value = v; o.textContent = v === "manual" ? "수동" : (v === "auto" ? "자동" : "테스트"); mode.appendChild(o); }); mode.value = st.run_mode || "manual";
    const runBtn = document.createElement("button"); runBtn.type = "button"; runBtn.textContent = "실행"; runBtn.title = "현재 탭에 이 도메인 모듈 JS를 주입하여 테스트 실행합니다"; runBtn.disabled = !applies; runBtn.dataset.moduleId = m.module_id; runBtn.dataset.action = "run_module";
    actions.appendChild(installLabel); actions.appendChild(enableLabel); actions.appendChild(mode); actions.appendChild(runBtn);
    item.appendChild(head); item.appendChild(meta); item.appendChild(stateLine); item.appendChild(actions); box.appendChild(item);
  });
  box.querySelectorAll("input,select").forEach(el => el.addEventListener("change", () => updateCurrentDomainModuleChoice(el)));
  box.querySelectorAll("button[data-action=run_module]").forEach(btn => btn.addEventListener("click", () => runDomainModule(btn.dataset.moduleId).catch(e => showToast("모듈 실행 실패: " + e.message, true))));
}
function escHtml(s) { return String(s || "").replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;"}[c])); }
function updateCurrentDomainModuleChoice(el) {
  if (!currentDomainScan || !currentDomainSettings) return;
  const id = el.dataset.moduleId || "";
  const action = el.dataset.action || "";
  const module = (currentDomainScan.modules || []).find(m => m.module_id === id) || { module_id:id };
  currentDomainSettings.modules = currentDomainSettings.modules || {};
  const old = currentDomainSettings.modules[id] || {};
  const next = Object.assign({}, old, { module_id:id, module_key:module.module_key || "", dev_key:module.dev_key || "", feature_key:module.feature_key || "", representative_domain:currentDomainScan.representative_domain || "", service_or_group:currentDomainScan.service_or_group || "", updated_at:new Date().toISOString() });
  if (action === "installed") { next.installed = el.checked ? 1 : 0; if (!next.installed) next.enabled = 0; }
  if (action === "enabled") { next.enabled = el.checked ? 1 : 0; if (next.enabled) next.installed = 1; }
  if (action === "run_mode") { next.run_mode = el.value || "manual"; }
  currentDomainSettings.modules[id] = next;
  renderDomainModules(currentDomainScan, currentDomainSettings);
}

async function runDomainModule(moduleId) {
  if (!currentDomainScan) await scanAndRenderDomainModules();
  const module = (currentDomainScan.modules || []).find(m => m.module_id === moduleId);
  if (!module) throw new Error("모듈을 찾을 수 없습니다: " + moduleId);
  updateCurrentDomainModuleChoice({ dataset:{ moduleId:moduleId, action:"enabled" }, checked:true });
  await saveCurrentDomainTabSettings();
  const closeBtn = document.getElementById("popupCloseBtn");
  if (closeBtn) closeBtn.addEventListener("click", () => { try { window.close(); } catch(e) {} });
  const tab = await activeTab();
  const res = await new Promise((resolve, reject) => {
    chrome.tabs.sendMessage(tab.id, { type:"AIWK_EXT4_DOMAIN_LOAD_MODULE", module:module }, r => {
      const le = chrome.runtime.lastError;
      if (le) { reject(new Error(le.message)); return; }
      if (!r || !r.ok) { reject(new Error((r && (r.error || r.code)) || "도메인 모듈 실행 실패")); return; }
      resolve(r);
    });
  });
  showToast("모듈 실행 완료: " + (module.title_ko || module.module_key || moduleId));
  return res;
}

async function scanAndRenderDomainModules() {
  const closeBtn = document.getElementById("popupCloseBtn");
  if (closeBtn) closeBtn.addEventListener("click", () => { try { window.close(); } catch(e) {} });
  const tab = await activeTab();
  currentDomainScan = await scanDomainModulesForTab(tab);
  const all = await getDomainSettingsAll();
  currentDomainSettings = all[String(tab.id)] || defaultDomainSettings(tab, currentDomainScan);
  renderDomainInfo(currentDomainScan, tab);
  renderDomainModules(currentDomainScan, currentDomainSettings);
  renderDomainTabHistory(all, tab);
  return currentDomainScan;
}
async function saveCurrentDomainTabSettings() {
  const closeBtn = document.getElementById("popupCloseBtn");
  if (closeBtn) closeBtn.addEventListener("click", () => { try { window.close(); } catch(e) {} });
  const tab = await activeTab();
  if (!currentDomainScan) await scanAndRenderDomainModules();
  const all = await getDomainSettingsAll();
  currentDomainSettings = Object.assign(defaultDomainSettings(tab, currentDomainScan), currentDomainSettings || {}, { tab_id:String(tab.id), title:tab.title || "", url:tab.url || "", updated_at:new Date().toISOString() });
  all[String(tab.id)] = currentDomainSettings;
  await setDomainSettingsAll(all);
  renderDomainTabHistory(all, tab);
  return currentDomainSettings;
}
async function deleteCurrentDomainTabSettings() {
  const closeBtn = document.getElementById("popupCloseBtn");
  if (closeBtn) closeBtn.addEventListener("click", () => { try { window.close(); } catch(e) {} });
  const tab = await activeTab();
  const all = await getDomainSettingsAll();
  delete all[String(tab.id)];
  await setDomainSettingsAll(all);
  currentDomainSettings = defaultDomainSettings(tab, currentDomainScan || await scanDomainModulesForTab(tab));
  renderDomainModules(currentDomainScan, currentDomainSettings);
  renderDomainTabHistory(all, tab);
  return { tab_id:String(tab.id) };
}
function renderDomainTabHistory(all, activeTabObj) {
  const sel = document.getElementById("domainTabHistory");
  if (!sel) return;
  const cur = activeTabObj && activeTabObj.id ? String(activeTabObj.id) : "";
  sel.innerHTML = "";
  const base = document.createElement("option"); base.value = ""; base.textContent = Object.keys(all || {}).length ? "저장된 탭 모듈 설정 선택" : "저장된 탭 모듈 설정 없음"; sel.appendChild(base);
  Object.keys(all || {}).sort((a,b) => Number(b)-Number(a)).forEach(k => {
    const r = all[k] || {}; const opt = document.createElement("option"); opt.value = k;
    const modCount = Object.keys(r.modules || {}).length;
    opt.textContent = (k === cur ? "현재탭 · " : "") + "tab:" + k + " / " + (r.representative_domain || "?") + " / folder:" + (r.folder_route || "-") + " / mod:" + modCount;
    sel.appendChild(opt);
  });
}
async function cloneSelectedDomainSettingsToCurrentTab() {
  const sel = document.getElementById("domainTabHistory");
  if (!sel || !sel.value) throw new Error("복제할 탭 설정을 먼저 선택하십시오.");
  const all = await getDomainSettingsAll();
  const src = all[String(sel.value)];
  if (!src) throw new Error("선택한 탭 설정이 없습니다.");
  const closeBtn = document.getElementById("popupCloseBtn");
  if (closeBtn) closeBtn.addEventListener("click", () => { try { window.close(); } catch(e) {} });
  const tab = await activeTab();
  all[String(tab.id)] = Object.assign({}, src, { tab_id:String(tab.id), title:tab.title || "", url:tab.url || "", cloned_from:String(sel.value), updated_at:new Date().toISOString() });
  await setDomainSettingsAll(all);
  currentDomainSettings = all[String(tab.id)];
  if (!currentDomainScan) currentDomainScan = await scanDomainModulesForTab(tab);
  renderDomainModules(currentDomainScan, currentDomainSettings);
  renderDomainTabHistory(all, tab);
  return { from:String(sel.value), to:String(tab.id) };
}
async function deleteSelectedDomainHistory() {
  const sel = document.getElementById("domainTabHistory");
  if (!sel || !sel.value) throw new Error("삭제할 탭 설정을 먼저 선택하십시오.");
  const all = await getDomainSettingsAll();
  const id = String(sel.value);
  delete all[id];
  await setDomainSettingsAll(all);
  const closeBtn = document.getElementById("popupCloseBtn");
  if (closeBtn) closeBtn.addEventListener("click", () => { try { window.close(); } catch(e) {} });
  const tab = await activeTab();
  renderDomainTabHistory(all, tab);
  return { tab_id:id };
}


function sendToActiveTabMessage(message) {
  const payload = message || {};
  return new Promise(async (resolve, reject) => {
    let tab = null;
    try { tab = await activeTab(); } catch(e) { tab = null; }
    const targetTabId = tab && tab.id ? String(tab.id) : "";
    const routedPayload = Object.assign({}, payload, { target_tab_id: targetTabId });
    function viaRuntime() {
      return new Promise((resolve2, reject2) => {
        chrome.runtime.sendMessage({ type:"AIWK_POPUP_TO_ACTIVE_TAB", target_tab_id:targetTabId, message:routedPayload }, res => {
          const le = chrome.runtime.lastError;
          if (le) { reject2(new Error(le.message)); return; }
          if (!res || !res.ok) { reject2(new Error((res && (res.error || res.code)) || "ACTIVE_TAB_MESSAGE_FAIL")); return; }
          resolve2(res);
        });
      });
    }
    try {
      if (targetTabId && targetTabId.indexOf("popup-") !== 0) {
        chrome.tabs.sendMessage(Number(targetTabId), routedPayload, async res => {
          const le = chrome.runtime.lastError;
          if (le) {
            const msg = String(le.message || "");
            if (msg.indexOf("Receiving end does not exist") >= 0 || msg.indexOf("Could not establish connection") >= 0 || msg.indexOf("Extension context invalidated") >= 0 || msg.indexOf("message port closed") >= 0) {
              try { resolve(await viaRuntime()); } catch(e2) { reject(e2); }
              return;
            }
            reject(new Error(msg));
            return;
          }
          resolve(res || { ok:1, direct:1, target_tab_id:targetTabId });
        });
        return;
      }
      viaRuntime().then(resolve).catch(reject);
    } catch(e) { viaRuntime().then(resolve).catch(reject); }
  });
}

function sendToChatGptText(text) {
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage({ type:"AIWK_FIND_CHATGPT_AND_INSERT_REPORT", text:String(text || "") }, res => {
      const le = chrome.runtime.lastError;
      if (le) { reject(new Error(le.message)); return; }
      if (!res || !res.ok) { reject(new Error((res && (res.error || res.code || res.message)) || "CHATGPT_INSERT_FAILED")); return; }
      resolve(res);
    });
  });
}
function wrapDiagForChatGpt(report, prefix) {
  const head = String(prefix || "AIWK EXT4 v045 진단 로그입니다. JSON을 분석해서 다음 수정 범위를 판단해줘.").trim();
  return head + "\n\nAIWK_DIAG_JSON_START\n" + JSON.stringify(report || {}, null, 2) + "\nAIWK_DIAG_JSON_END";
}
async function runHomeSelfTest() {
  const cfg = getForm();
  const closeBtn = document.getElementById("popupCloseBtn");
  if (closeBtn) closeBtn.addEventListener("click", () => { try { window.close(); } catch(e) {} });
  const tab = await activeTab();
  const res = await sendToActiveTabMessage({ type:"AIWK_EXT4_HOME_SELFTEST", config:cfg, tab_id:tab && tab.id ? String(tab.id) : "", source:"popup" });
  return res;
}

async function sendCurrentDiagToWeb1() {
  const closeBtn = document.getElementById("popupCloseBtn");
  if (closeBtn) closeBtn.addEventListener("click", () => { try { window.close(); } catch(e) {} });
  const tab = await activeTab();
  const res = await sendToActiveTabMessage({ type:"AIWK_BUILD_DIAGNOSTIC_REPORT", notes:["popup WEB1 로그전달", "home.yjm.kr/aiwk WEB1 debug bridge", "token 원문 제외", "v157-heart-title-sticky-common"] });
  const report = res && res.report ? res.report : { ok:0, code:"DIAG_REPORT_MISSING", response:res, tab_id:tab && tab.id ? tab.id : "" };
  report.popup = { version:"v157-heart-title-sticky-common", target:"WEB1", tab_id:tab && tab.id ? String(tab.id) : "", form:{ group:getForm().group, project:getForm().project, roomId:getForm().roomId, clientId:getForm().clientId, token_present:getForm().authToken ? 1 : 0 } };
  const delivered = await new Promise((resolve, reject) => {
    chrome.runtime.sendMessage({ type:"AIWK_FIND_WEB1_AND_DELIVER_REPORT", report:report }, res2 => {
      const le = chrome.runtime.lastError;
      if (le) { reject(new Error(le.message)); return; }
      if (!res2 || !res2.ok) { reject(new Error((res2 && (res2.error || res2.code || res2.message)) || "WEB1_DELIVER_FAILED")); return; }
      resolve(res2);
    });
  });
  return { ok:1, report_len:JSON.stringify(report).length, web1:delivered };
}

async function sendCurrentDiagToChatGpt() {
  const closeBtn = document.getElementById("popupCloseBtn");
  if (closeBtn) closeBtn.addEventListener("click", () => { try { window.close(); } catch(e) {} });
  const tab = await activeTab();
  const res = await sendToActiveTabMessage({ type:"AIWK_BUILD_DIAGNOSTIC_REPORT", notes:["popup ChatGPT 직접전달", "token 원문 제외", "v157-heart-title-sticky-common"] });
  const report = res && res.report ? res.report : { ok:0, code:"DIAG_REPORT_MISSING", response:res, tab_id:tab && tab.id ? tab.id : "" };
  report.popup = { version:"v157-heart-title-sticky-common", tab_id:tab && tab.id ? String(tab.id) : "", form:{ group:getForm().group, project:getForm().project, roomId:getForm().roomId, clientId:getForm().clientId, token_present:getForm().authToken ? 1 : 0 } };
  const text = wrapDiagForChatGpt(report);
  const sent = await sendToChatGptText(text);
  return { ok:1, report_len:JSON.stringify(report).length, chatgpt:sent };
}


function commControlsEnabled(cfg) {
  cfg = cfg || getForm();
  const token = String(cfg.authToken || cfg.token || "").trim();
  const role = String(cfg.authRole || "").trim();
  return !!token && role && role !== "guest";
}
function setCommControlsEnabled(enabled) {
  const box = document.getElementById("commControlBox");
  if (box) {
    box.classList.toggle("is-disabled", !enabled);
    box.title = enabled ? "WSS/BUS/NR 통신 제어가 활성화되었습니다." : "토큰 로그인 성공 후 WSS/BUS/NR 제어가 활성화됩니다.";
  }
  ["wssConnectBtn","wssDisconnectBtn","wssKeepChk","wssRetrySec","busConnectBtn","busDisconnectBtn","busKeepChk","busRetrySec","nrConnectBtn","nrDisconnectBtn","nrKeepChk","nrRetrySec"].forEach(id => {
    const el = document.getElementById(id);
    if (el) el.disabled = !enabled;
  });
}
function badgeClass(level) {
  if (level === "ok") return "comm-badge ok";
  if (level === "warn") return "comm-badge warn";
  if (level === "off") return "comm-badge off";
  return "comm-badge bad";
}
function setBadge(id, text, level, title) {
  const el = document.getElementById(id);
  if (!el) return;
  el.className = badgeClass(level);
  el.textContent = text || "-";
  el.title = title || "";
}
function renderChatInbox(comm) {
  const box = document.getElementById("aiwkChatInbox");
  if (!box) return;
  const list = comm && Array.isArray(comm.recent_messages) ? comm.recent_messages.slice(-8) : [];
  if (!list.length) { box.textContent = "수신 대기"; return; }
  box.innerHTML = "";
  list.slice().reverse().forEach(function(m) {
    const line = document.createElement("div");
    line.className = "rx-line";
    const d = m.at ? new Date(m.at) : null;
    const time = d ? d.toLocaleTimeString() : "";
    const action = m.action || m.type || "message";
    const from = m.from || "?";
    const text = m.text ? " / " + String(m.text).slice(0, 300) : "";
    line.textContent = (time ? "[" + time + "] " : "") + action + " ← " + from + text;
    box.appendChild(line);
  });
}
function pad4(n) { return String(Math.max(0, parseInt(n || 0, 10) || 0)).padStart(4, "0"); }
function commCountText(comm) {
  const c = (comm && comm.counters) || {};
  return "R:" + pad4(c.rx) + " T:" + pad4(c.tx);
}
function subCountText(obj) {
  return "R:" + pad4(obj && obj.rx_count) + " T:" + pad4(obj && obj.tx_count);
}
function renderCommStatusFromReport(report) {
  const comm = report && report.comm ? report.comm : null;
  const summary = document.getElementById("commStatusSummary");
  if (!comm) {
    if (summary) summary.textContent = "통신 상태 확인 전";
    setBadge("wssStatusBadge", "끊김", "bad", "WSS 상태 확인 전");
    setBadge("busStatusBadge", "미등록", "bad", "BUS 상태 확인 전");
    setBadge("nrStatusBadge", "끊김", "bad", "NR 상태 확인 전");
    return;
  }
  renderChatInbox(comm);
  updatePopupTitleBadge(statusInfoFromComm(comm, null));
  const cmdBlock = comm.cmd_block || null;
  if (cmdBlock && cmdBlock.nr_related) {
    const reason = cmdBlock.reason || "auth_required";
    const help = cmdBlock.help || "NR Ping CMD가 차단되었습니다. 보낸 쪽 client의 [프로필 저장] -> [WSS 인증등록]을 다시 실행하십시오.";
    const text = "NR Ping CMD 차단\n오류 원인: " + reason + "\n해결책: " + help;
    setInlineNotice("NR Ping CMD 차단: " + reason + " / WSS 연결 자체는 유지됩니다.", "err");
    if (lastAuthBlockToastKey !== "NR:" + reason) {
      lastAuthBlockToastKey = "NR:" + reason;
      showToast(text, true);
    }
  } else if (comm.auth_blocked) {
    const reason = comm.auth_block_reason || "auth_required";
    const help = comm.auth_block_help || "client_id/token/권한을 확인한 뒤 [프로필 저장] -> [WSS 인증등록]을 다시 실행하십시오.";
    const text = "WSS 인증 실패 · 자동 재시도 중단\n오류 원인: " + reason + "\n해결책: " + help;
    setInlineNotice("WSS 인증 실패: " + reason + " / " + help, "err");
    if (lastAuthBlockToastKey !== "WSS:" + reason) {
      lastAuthBlockToastKey = "WSS:" + reason;
      showToast(text, true);
    }
  } else {
    lastAuthBlockToastKey = "";
  }
  const wss = comm.wss || {};
  const bus = comm.tabbus || {};
  const nr = comm.nodered || {};
  const duplicateRoute = !!wss.duplicate_route || String(wss.last_error || "").indexOf("동일 route/client") >= 0 || String(wss.last_error || "").indexOf("same_route_client_reconnected") >= 0;
  const wssLevel = wss.open ? "ok" : (wss.connecting ? "warn" : (duplicateRoute ? "warn" : "bad"));
  const busLevel = bus.registered ? "ok" : (bus.ok ? "warn" : "bad");
  const nrLevel = nr.ok ? "ok" : (nr.auto_keep ? "warn" : "off");
  const wssVerified = !!(wss.verified_ok || wss.last_roundtrip_at);
  const nrVerified = !!(nr.ok && nr.last_received_at);
  setBadge("wssStatusBadge", (wss.open ? (wssVerified ? "✅" : "🟢") : (wss.connecting ? "🟡" : (duplicateRoute ? "♻️" : "🔴"))) + " " + subCountText(wss), wssVerified ? "ok" : wssLevel, wss.last_error || (wssVerified ? "송수신 성공" : ""));
  setBadge("busStatusBadge", (bus.registered ? "✅" : (bus.ok ? "🟡" : "🔴")) + " " + subCountText(bus), busLevel, bus.last_error || "");
  setBadge("nrStatusBadge", (nrVerified ? "✅" : (nr.ok ? "🟢" : (nr.auto_keep ? "🟡" : "▫️"))) + " " + subCountText(nr), nrVerified ? "ok" : nrLevel, nr.last_error || (nrVerified ? "Node-RED 송수신 성공" : (nr.auto_keep ? "N1(Node-RED) ping 반복 확인 중" : "Node-RED 테스트 전")));
  if (summary) summary.textContent = cmdBlock && cmdBlock.nr_related ? ("NR차단: " + (cmdBlock.reason || "auth_required")) : (comm.auth_blocked ? ("WSS중단: " + (comm.auth_block_reason || "auth_required")) : commCountText(comm));
  const wssKeep = document.getElementById("wssKeepChk");
  if (wssKeep) wssKeep.checked = !!wss.auto_reconnect;
  const busKeep = document.getElementById("busKeepChk");
  if (busKeep) busKeep.checked = !!(comm.tabbus && comm.tabbus.auto_keep);
  syncBusSecInput(Math.round((parseInt(bus.keep_ms || 0, 10) || 10000) / 1000));
  const nrKeep = document.getElementById("nrKeepChk");
  if (nrKeep) nrKeep.checked = !!(comm.nodered && comm.nodered.auto_keep);
  syncRetrySecInputs(Math.round((parseInt(wss.reconnect_ms || 0, 10) || 10000) / 1000));
}
async function refreshCommStatus() {
  try {
    const res = await sendToActiveTabMessage({ type:"AIWK_BUILD_DIAGNOSTIC_REPORT", notes:["popup comm status", "v157-heart-title-sticky-common"] });
    if (res && res.report) renderCommStatusFromReport(res.report);
    return res;
  } catch(e) {
    const summary = document.getElementById("commStatusSummary");
    if (summary) summary.textContent = "통신 상태 확인 실패: " + e.message; setInlineNotice("통신 상태 확인 실패: " + e.message, "err");
    return { ok:0, error:e.message };
  }
}
async function sendCommControl(action, extra) {
  extra = extra || {};
  let cfg = getForm();
  cfg = await validateLogin(cfg);
  if (!extra.skip_save) await saveConfig(cfg);
  const tab = await activeTab();
  const checked = await requireCurrentTabConfig(cfg, tab);
  try { await chrome.storage.local.set({ [STORE_DEBUG_LAST_REGISTER]: { at:Date.now(), action, clientId:cfg.clientId, group:cfg.group, project:cfg.project, roomId:cfg.roomId, url5:routeKeyFromUrl(tab && tab.url || ""), tab_id:checked.tab_id, token_present:!!cfg.authToken, source:"popup.sendCommControl" } }); } catch(e) {}
  const res = await sendToActiveTabMessage({ type:"AIWK_EXT4_COMM_CONTROL", action:action, config:cfg, extra:extra || {}, target_tab_id:checked.tab_id, required_tab_config_key:checked.key });
  await refreshCommStatus();
  return res;
}

async function buildPopupDebugSnapshot() {
  const closeBtn = document.getElementById("popupCloseBtn");
  if (closeBtn) closeBtn.addEventListener("click", () => { try { window.close(); } catch(e) {} });
  const tab = await activeTab();
  const form = getForm();
  const vault = await getClientProfileVault();
  const domainAll = await getDomainSettingsAll();
  let contentDiag = null;
  try {
    const res = await sendToActiveTabMessage({ type:"AIWK_BUILD_DIAGNOSTIC_REPORT", notes:["popup debug snapshot", "v157-heart-title-sticky-common"] });
    contentDiag = res && res.report ? stripUrlMd5({ ok:1, aiwk_version:res.report.aiwk_version, comm:res.report.comm, windows:res.report.windows }) : stripUrlMd5(res);
  } catch(e) {
    contentDiag = { ok:0, error:e.message };
  }
  return {
    kind:"AIWK_POPUP_DEBUG_SNAPSHOT",
    version:"v157-heart-title-sticky-common",
    at:new Date().toISOString(),
    tab:{ id:tab && tab.id ? String(tab.id) : "", title:tab && tab.title || "", url:tab && tab.url || "" },
    form:{ group:form.group, project:form.project, roomId:form.roomId, clientId:form.clientId, token_present:form.authToken ? 1 : 0 },
    role_hint:{ A:"실행 EXT", WEB1:"home WEB 관제", ADMIN1:"관리자 전체권한", CHAT1:"ChatGPT 전달", HOST1:"서버/PHP 제어" },
    domain_scan: currentDomainScan ? { matched:currentDomainScan.matched, levels:(currentDomainScan.loaded_levels||[]).map(x=>x.key), representative_domain:currentDomainScan.representative_domain, folder_route:currentDomainScan.folder_route, module_count:(currentDomainScan.modules||[]).length } : null,
    saved:{ client_profile_count:Object.keys(vault||{}).length, domain_tab_count:Object.keys(domainAll||{}).length },
    content_diag:contentDiag
  };
}
function buildCompactDebugText(snap) {
  snap = snap || {};
  const form = snap.form || {};
  const tab = snap.tab || {};
  const comm = snap.content_diag && snap.content_diag.comm ? snap.content_diag.comm : {};
  const wss = comm.wss || {};
  const nr = comm.nodered || {};
  const route = comm.route || {};
  const cmd = comm.cmd_block || null;
  const lines = [];
  lines.push("AIWK DEBUG COMPACT");
  lines.push("ver: " + (snap.version || "v157-heart-title-sticky-common"));
  lines.push("client: " + (form.clientId || "") + " / group.project: " + (form.group || "") + "." + (form.project || ""));
  lines.push("room_token_present: " + (form.roomId ? "yes" : "no") + " / auth_token_present: " + (form.token_present ? "yes" : "no"));
  lines.push("url5: " + (route.route_key || route.url_key || "") + " / tab: " + (tab.id || ""));
  lines.push("WSS: open=" + (!!wss.open) + " auto=" + (!!wss.auto_reconnect) + " speak=" + !!(wss.auth && wss.auth.can_speak) + " err=" + (wss.last_error || ""));
  lines.push("NR: status=" + (nr.status || "") + " ok=" + (!!nr.ok) + " auto=" + (!!nr.auto_keep) + " err=" + (nr.last_error || ""));
  if (cmd) {
    lines.push("last_cmd_block: reason=" + (cmd.reason || "") + " nr_related=" + (!!cmd.nr_related));
    lines.push("from: " + (cmd.from || ""));
    lines.push("to: " + (cmd.to || ""));
    lines.push("act: " + (cmd.act || ""));
    lines.push("fix: " + (cmd.help || "보낸 쪽 client의 프로필 저장 후 WSS 인증등록을 다시 실행하십시오."));
  } else if (comm.auth_blocked) {
    lines.push("last_auth_block: " + (comm.auth_block_reason || ""));
    lines.push("fix: " + (comm.auth_block_help || "프로필 저장 후 WSS 인증등록을 다시 실행하십시오."));
  }
  lines.push("title: " + (tab.title || ""));
  return lines.join("\n");
}
async function refreshPopupDebug() {
  const ta = document.getElementById("debugHubText");
  const snap = await buildPopupDebugSnapshot();
  const text = buildCompactDebugText(snap);
  if (ta) ta.value = text;
  return text;
}
async function copyPopupDebug() {
  const text = await refreshPopupDebug();
  if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); return { ok:1, method:"clipboard", len:text.length }; }
  const ta = document.getElementById("debugHubText");
  if (ta) { ta.focus(); ta.select(); document.execCommand("copy"); return { ok:1, method:"execCommand", len:text.length }; }
  return { ok:0, error:"clipboard unavailable" };
}

let aiwkPingLoopTimer = null;
let aiwkPingLoopRunning = false;
function aiwkPingLoopSecValue() {
  const el = document.getElementById("aiwkPingLoopSec");
  const n = parseInt(el && el.value || "10", 10);
  return Math.max(1, isNaN(n) ? 10 : n);
}
function setPingLoopStatus(text, level) {
  const el = document.getElementById("aiwkPingLoopStatus");
  if (!el) return;
  el.textContent = text || "";
  el.className = "aiwk-ping-loop-status " + (level || "info");
}
function stopPingLoop(reason) {
  if (aiwkPingLoopTimer) clearTimeout(aiwkPingLoopTimer);
  aiwkPingLoopTimer = null;
  aiwkPingLoopRunning = false;
  const chk = document.getElementById("aiwkPingLoopChk");
  if (chk) chk.checked = false;
  setPingLoopStatus(reason ? ("중단: " + reason) : "중단", "off");
}
async function startPingLoop(sendFn) {
  if (aiwkPingLoopRunning) return;
  const chk = document.getElementById("aiwkPingLoopChk");
  aiwkPingLoopRunning = true;
  if (chk) chk.checked = true;
  const tick = async () => {
    if (!aiwkPingLoopRunning) return;
    const toEl = document.getElementById("aiwkChatTo");
    const target = String(toEl && toEl.value || "").trim() || aiwkDefaultChatTarget("n1");
    const sec = aiwkPingLoopSecValue();
    const at = new Date().toLocaleTimeString();
    try {
      setPingLoopStatus("진행: " + sec + "초 간격 → " + target + " / " + at, "run");
      await sendFn("ping", { silent:true, loop:true });
    } catch (err) {
      const em = String(err && err.message || err || "");
      setPingLoopStatus("오류: " + em, "err");
      if (/sender_not_registered|connect_not_allowed|auth_fail|AUTH|TOKEN|not registered/i.test(em)) {
        stopPingLoop("인증/등록 오류");
        return;
      }
    }
    if (aiwkPingLoopRunning) aiwkPingLoopTimer = setTimeout(tick, sec * 1000);
  };
  tick();
}

async function shutdownAll() {
  stopPingLoop("전체 종료");
  let endRes = null;
  try { endRes = await markCurrentTabEnded("shutdown_all"); } catch(e) { endRes = { ok:0, error:e && e.message ? e.message : String(e) }; }
  try { await sendCommControl("nr_keep", { checked:false }); } catch(e) {}
  try { await sendCommControl("wss_keep", { checked:false }); } catch(e) {}
  try { await sendCommControl("bus_keep", { checked:false }); } catch(e) {}
  try { await sendCommControl("nr_disconnect"); } catch(e) {}
  try { await sendCommControl("bus_disconnect"); } catch(e) {}
  try { await sendCommControl("wss_disconnect"); } catch(e) {}
  try { await chrome.runtime.sendMessage({ type:"AIWK_EXT4_CONTROL_FROM_POPUP", action:"close_all" }); } catch(e) {}
  await markLoggedOut("shutdown_all");
  setUserClosedBadge();
  setCommControlsEnabled(false);
  setInlineNotice("전체 종료 완료: 반복 Ping/WSS유지/NR유지/BUS/WSS를 중단했습니다." + (endRes && endRes.ok ? (" / tab end=" + endRes.end_label) : ""), "info");
  try { window.close(); } catch(e) {}
  return { ok:1 };
}

document.addEventListener("DOMContentLoaded", async () => {
  await migratePopupStorageOnce().catch(()=>{});
  updateAuthButtons(await getAuthState().catch(()=>({authenticated:false,connected:false})));
  const title = document.getElementById("popup-title");
  if (title && chrome.runtime?.getManifest) {
    const span = title.querySelector("span");
    if (span) span.textContent = "AIWK EXT4 접속창 v" + chrome.runtime.getManifest().version;
  }
  const closeBtn = document.getElementById("popupCloseBtn");
  if (closeBtn) closeBtn.addEventListener("click", () => { try { window.close(); } catch(e) {} });
  const tab = await activeTab();
  updatePopupTabBadge(tab);
  let routeUseEnabled = true;
  try { const g = await chrome.storage.local.get([STORE_GLOBAL_USE_ENABLED]); if (g[STORE_GLOBAL_USE_ENABLED] === false) routeUseEnabled = false; } catch(e) {}
  try {
    const routeUseStatus = await sendToActiveTabMessage({ type:"AIWK_ROUTE_USE_STATUS" });
    routeUseEnabled = !routeUseStatus || routeUseStatus.enabled !== 0;
  } catch(e) {}
  if (routeUseEnabled) await ensureCurrentTabNewConfig(tab).catch(()=>{});
  const useChk = document.getElementById("aiwkUseChk");
  if (useChk) {
    const key = tab && tab.id ? tabRouteConfigKey(tab) : "";
    const obj = key ? await chrome.storage.local.get([key]).catch(()=>({})) : {};
    const row = key ? (obj[key] || {}) : {};
    const tabState = row.tabs && row.tabs[String(tab.id)] ? row.tabs[String(tab.id)] : {};
    useChk.checked = routeUseEnabled && tabState.enabled !== false && !tabState.ended;
    useChk.addEventListener("change", async () => {
      try { await setCurrentRouteUse(!!useChk.checked); }
      catch(e) { showToast("사용 변경 실패: " + e.message, true); useChk.checked = !useChk.checked; }
    });
  }
  const keys = [STORE_CONFIG, STORE_CLIENT_HISTORY, STORE_ROUTE_PROFILE_INDEX, STORE_PROFILE_V152_INDEX, STORE_GLOBAL_USE_ENABLED]; if (tab && tab.id) keys.push(tabRouteConfigKey(tab));
  const saved = await chrome.storage.local.get(keys);
  const oldGlobal = saved[STORE_CONFIG] || {}; const rawTabCfg = tab && tab.id ? (saved[tabRouteConfigKey(tab)] || {}) : {};
  const routeIndex = normalizeRouteProfileIndex(saved[STORE_ROUTE_PROFILE_INDEX] || {});
  const routeIndexCfg = routeProfileForTab(routeIndex, tab) || {};
  const profileV152Cfg = await getProfileV152ForTab(tab).catch(()=>null) || {};
  const tabCfg = isEndedTabConfig(rawTabCfg) ? Object.assign({}, routeIndexCfg, profileV152Cfg) : Object.assign({}, routeIndexCfg, rawTabCfg, profileV152Cfg);
  const history = normalizeClientHistory(saved[STORE_CLIENT_HISTORY] || []);
  const cfg = Object.assign({}, DEFAULTS, globalConfigOnly(oldGlobal), {
    clientId:tabCfg.clientId || tabCfg.client_id || "",
    group:tabCfg.group || oldGlobal.group || DEFAULTS.group,
    project:tabCfg.project || oldGlobal.project || DEFAULTS.project,
    roomId:tabCfg.roomId || tabCfg.room_id || "",
    authToken:tabCfg.authToken || tabCfg.token || "",
    authMode:tabCfg.authMode || "empty",
    authRole:tabCfg.authRole || "guest",
    authUserType:tabCfg.authUserType || "",
    authPayload:tabCfg.authPayload || {}
  });
  if (isEndedTabConfig(rawTabCfg)) {
    setInlineNotice("현재 tab_id는 종료 상태(" + (rawTabCfg.end_label || (String(tab && tab.id || "") + "END")) + ")입니다. 자동 로딩하지 않습니다. 다시 접속하려면 client_id를 선택/입력 후 인증등록을 누르십시오.", "warn");
  } else if (String(rawTabCfg.status || rawTabCfg.aiwk_status || "").toUpperCase() === "NEW" || !rawTabCfg.clientId) {
    setInlineNotice("최초/현재 탭 미배정 상태입니다. client_id/token을 선택한 뒤 [프로필 저장] 또는 [WSS 인증등록]을 누르면 현재 tab_id에 ACTIVE로 배정됩니다.", "info");
  }
  setForm(cfg);
  updatePopupTitleBadge(statusInfoFromComm(null, await getAuthState().catch(()=>({authenticated:false,connected:false}))));
  renderTokenInfo(cfg);
  updateAuthButtons(await getAuthState().catch(()=>({authenticated:false,connected:false})));
  ensureChatTarget();
  setCommControlsEnabled(commControlsEnabled(cfg));
  await refreshClientHistoryUI(tab);
  scanAndRenderDomainModules().catch(e => { renderDomainInfo({ matched:0, url:{ hostname:"" }, message:e.message }, tab); });
  ["wsUrl","clientId","role","group","project","roomId","authToken"].forEach(id => {
    const e = document.getElementById(id); if (!e) return;
    const onEdit = () => { const cfgNow = getForm(); renderTokenInfo(cfgNow); ensureChatTarget(); setInlineNotice("입력값은 아직 저장되지 않았습니다. [프로필 저장] 또는 [WSS 인증등록] 때만 v152 정본 저장소에 기록됩니다.", "info"); };
    e.addEventListener("input", onEdit);
    e.addEventListener("change", onEdit);
  });
  const historySelect = document.getElementById("clientIdHistory");
  if (historySelect) historySelect.addEventListener("change", () => {
    const v = historySelect.value || "";
    const opt = historySelect.options[historySelect.selectedIndex];
    if (v) {
      document.getElementById("clientId").value = v;
      if (opt) {
        if (opt.dataset.wsUrl) document.getElementById("wsUrl").value = opt.dataset.wsUrl;
        if (opt.dataset.role) document.getElementById("role").value = normalizeRole(opt.dataset.role);
        if (opt.dataset.group) document.getElementById("group").value = opt.dataset.group;
        if (opt.dataset.project) document.getElementById("project").value = opt.dataset.project;
        if (opt.dataset.roomId) document.getElementById("roomId").value = opt.dataset.roomId;
        document.getElementById("authToken").value = opt.dataset.authToken || "";
        const cfgNow = getForm();
        cfgNow.authMode = opt.dataset.authMode || "empty";
        cfgNow.authRole = opt.dataset.authRole || "guest";
        cfgNow.authUserType = opt.dataset.authUserType || "";
        try { cfgNow.authPayload = opt.dataset.authPayload ? JSON.parse(opt.dataset.authPayload) : {}; } catch(e) { cfgNow.authPayload = {}; }
        renderTokenInfo(cfgNow);
        showToast("client_id/token 로드: " + v + " / tab:" + (opt.dataset.tabId || "?") + " / 아직 저장 안 됨");
        setInlineNotice("선택한 프로필을 폼에만 불러왔습니다. 현재 url5에 확정하려면 [프로필 저장] 또는 [WSS 인증등록]을 누르십시오.", "info");
      }
    }
  });
  const deleteHistoryBtn = document.getElementById("deleteClientHistoryBtn");
  if (deleteHistoryBtn) deleteHistoryBtn.addEventListener("click", async () => {
    try {
      const res = await deleteSelectedClientHistory();
      showToast("삭제 완료: " + res.clientId + " / tab:" + res.tabId);
    } catch(e) { showToast("삭제 실패: " + e.message, true); }
  });
  const cleanHistoryBtn = document.getElementById("cleanClientHistoryBtn");
  if (cleanHistoryBtn) cleanHistoryBtn.addEventListener("click", async () => {
    try {
      const before = await scanProfileCleanupCandidates().catch(()=>null);
      const res = await cleanupClientProfiles();
      const stale = await cleanupStaleTabConfigs().catch(()=>({removed_tab_config:0}));
      const after = await scanProfileCleanupCandidates().catch(()=>null);
      const conflictText = before && before.conflict_count ? " / 충돌후보=" + before.conflict_count : "";
      showToast("정리/스캔 완료: profile=" + res.profile_count + " / tab=" + res.history_count + " / stale=" + (stale.removed_tab_config || 0) + conflictText + (after && after.conflict_count ? " / 남은충돌=" + after.conflict_count : ""));
    } catch(e) { showToast("정리 실패: " + e.message, true); }
  });
  const scanDomainBtn = document.getElementById("scanDomainBtn");
  if (scanDomainBtn) scanDomainBtn.addEventListener("click", async () => { try { const res = await scanAndRenderDomainModules(); showToast("도메인 검사 완료: " + (res.representative_domain || "없음") + " / modules=" + ((res.modules || []).length)); } catch(e) { showToast("도메인 검사 실패: " + e.message, true); } });
  const saveTabDomainBtn = document.getElementById("saveTabDomainBtn");
  if (saveTabDomainBtn) saveTabDomainBtn.addEventListener("click", async () => { try { const res = await saveCurrentDomainTabSettings(); showToast("탭에저장 완료: tab=" + res.tab_id); } catch(e) { showToast("탭에저장 실패: " + e.message, true); } });
  const deleteTabDomainBtn = document.getElementById("deleteTabDomainBtn");
  if (deleteTabDomainBtn) deleteTabDomainBtn.addEventListener("click", async () => { try { const res = await deleteCurrentDomainTabSettings(); showToast("탭 설정삭제 완료: tab=" + res.tab_id); } catch(e) { showToast("탭 설정삭제 실패: " + e.message, true); } });
  const cloneDomainTabBtn = document.getElementById("cloneDomainTabBtn");
  if (cloneDomainTabBtn) cloneDomainTabBtn.addEventListener("click", async () => { try { const res = await cloneSelectedDomainSettingsToCurrentTab(); showToast("현재탭복제 완료: " + res.from + " → " + res.to); } catch(e) { showToast("현재탭복제 실패: " + e.message, true); } });
  const deleteDomainHistoryBtn = document.getElementById("deleteDomainHistoryBtn");
  if (deleteDomainHistoryBtn) deleteDomainHistoryBtn.addEventListener("click", async () => { try { const res = await deleteSelectedDomainHistory(); showToast("저장 탭 설정 삭제: " + res.tab_id); } catch(e) { showToast("저장 탭 설정 삭제 실패: " + e.message, true); } });
  const homeSelfTestBtn = document.getElementById("homeSelfTestBtn");
  if (homeSelfTestBtn) homeSelfTestBtn.addEventListener("click", async () => { try { const res = await runHomeSelfTest(); showToast("home 셀프테스트 완료: " + (res && res.code ? res.code : "OK")); } catch(e) { showToast("home 셀프테스트 실패: " + e.message, true); } });
  const sendDiagWeb1Btn = document.getElementById("sendDiagWeb1Btn");
  if (sendDiagWeb1Btn) sendDiagWeb1Btn.addEventListener("click", async () => { try { const res = await sendCurrentDiagToWeb1(); showToast("WEB1로 로그전달 완료 len=" + (res.report_len || 0)); } catch(e) { showToast("WEB1 로그전달 실패: " + e.message, true); } });
  const sendDiagChatGptBtn = document.getElementById("sendDiagChatGptBtn");
  if (sendDiagChatGptBtn) sendDiagChatGptBtn.addEventListener("click", async () => { try { const res = await sendCurrentDiagToChatGpt(); showToast("ChatGPT 입력 완료 len=" + (res.report_len || 0)); } catch(e) { showToast("ChatGPT 직접전달 실패: " + e.message, true); } });


  async function openRelayWindowFromPopup() {
    const relayUrl = chrome.runtime.getURL("AIWK_RELAY/side_panel.html?mode=window");
    const baseUrl = chrome.runtime.getURL("AIWK_RELAY/side_panel.html");
    const wins = await chrome.windows.getAll({ populate:true });
    for (const w of wins || []) {
      for (const t of (w.tabs || [])) {
        const u = String(t.url || "");
        if (u === relayUrl || u.indexOf(baseUrl) === 0) {
          await chrome.windows.update(w.id, { focused:true });
          if (t.id) await chrome.tabs.update(t.id, { active:true });
          return { ok:1, method:"popup.windows.update", window_id:w.id, tab_id:t.id || 0 };
        }
      }
    }
    let cur = null;
    try { cur = await chrome.windows.getCurrent(); } catch(e) { cur = null; }
    const width = 520;
    const height = 780;
    const left = cur && Number.isFinite(cur.left) && Number.isFinite(cur.width) ? Math.max(0, cur.left + cur.width - width - 24) : undefined;
    const top = cur && Number.isFinite(cur.top) ? Math.max(0, cur.top + 48) : undefined;
    const opt = { url:relayUrl, type:"popup", width, height, focused:true };
    if (left !== undefined) opt.left = left;
    if (top !== undefined) opt.top = top;
    const win = await chrome.windows.create(opt);
    return { ok:1, method:"popup.windows.create", window_id:win && win.id || 0 };
  }

  const openRelayPanelBtn = document.getElementById("openRelayPanelBtn");
  if (openRelayPanelBtn) openRelayPanelBtn.addEventListener("click", async () => {
    try {
      const tabNow = await activeTab();
      const res = await chrome.runtime.sendMessage({ type:"AIWK_OPEN_RELAY_PANEL", source:"popup_open_button", target_tab_id:tabNow && tabNow.id ? String(tabNow.id) : "" });
      showToast("Relay 창 열기: " + (res && res.ok ? "OK" : ((res && res.code) || "확인 필요")));
    } catch(e) {
      try {
        const res = await openRelayWindowFromPopup();
        showToast("Relay 창 열기: " + (res && res.ok ? "OK" : ((res && res.code) || "확인 필요")));
      } catch(e2) {
        showToast("Relay 창 열기 실패: " + ((e2 && e2.message) || (e && e.message) || String(e2 || e)), true);
      }
    }
  });

  const nrEditorLink = document.getElementById("nrEditorLink");
  if (nrEditorLink) nrEditorLink.addEventListener("click", async (ev) => {
    ev.preventDefault();
    try { await chrome.tabs.create({ url:"http://127.0.0.1:1880/" }); showToast("Node-RED 편집기 열기: http://127.0.0.1:1880/"); } catch(e) { showToast("Node-RED 링크 열기 실패: " + e.message, true); }
  });
  const chatTo = document.getElementById("aiwkChatTo");
  const chatMsg = document.getElementById("aiwkChatMessage");
  const chatSendBtn = document.getElementById("aiwkChatSendBtn");
  const chatPingBtn = document.getElementById("aiwkChatPingBtn");
  const chatTargetN1Btn = document.getElementById("aiwkChatTargetN1Btn");
  const chatTargetSelfBtn = document.getElementById("aiwkChatTargetSelfBtn");
  const pingLoopChk = document.getElementById("aiwkPingLoopChk");
  const pingLoopSec = document.getElementById("aiwkPingLoopSec");
  if (chatTo && !String(chatTo.value || "").trim()) chatTo.value = aiwkDefaultChatTarget("n1");
  async function sendQuickChat(kind, opts) {
    opts = opts || {};
    const target = String(chatTo && chatTo.value || "").trim() || aiwkDefaultChatTarget("n1");
    const text = String(chatMsg && chatMsg.value || "").trim();
    if (kind !== "ping" && !text) { showToast("메시지를 입력하십시오.", true); if (chatMsg) chatMsg.focus(); return; }
    try {
      const action = kind === "ping" ? "ping_send" : "chat_send";
      const res = await sendCommControl(action, { to:target, text:text, loop:!!opts.loop });
      setInlineNotice((kind === "ping" ? "Ping" : "채팅") + " 전송. 응답 수신 시 송수신✅로 바뀝니다.", "info");
      if (!opts.silent) showToast((kind === "ping" ? "Ping 전송" : "채팅 전송") + ": " + target + " / " + ((res && res.ok) ? "OK" : "확인 필요"));
      if (kind !== "ping" && chatMsg) chatMsg.value = "";
      return res;
    } catch(e) {
      if (!opts.silent) showToast((kind === "ping" ? "Ping" : "채팅") + " 실패: " + e.message, true);
      throw e;
    }
  }
  if (chatSendBtn) chatSendBtn.addEventListener("click", () => sendQuickChat("chat"));
  if (chatPingBtn) chatPingBtn.addEventListener("click", () => sendQuickChat("ping"));
  if (pingLoopChk) pingLoopChk.addEventListener("change", () => {
    if (pingLoopChk.checked) startPingLoop(sendQuickChat);
    else stopPingLoop("사용자 중단");
  });
  if (pingLoopSec) pingLoopSec.addEventListener("change", () => {
    if (aiwkPingLoopRunning) setPingLoopStatus("간격 변경: " + aiwkPingLoopSecValue() + "초. 다음 ping부터 적용", "info");
  });
  if (chatMsg) chatMsg.addEventListener("keydown", ev => { if (ev.key === "Enter" && !ev.shiftKey) { ev.preventDefault(); sendQuickChat("chat"); } });
  if (chatTargetN1Btn) chatTargetN1Btn.addEventListener("click", () => { if (chatTo) chatTo.value = aiwkDefaultChatTarget("n1"); showToast("to=N1 Node-RED"); });
  if (chatTargetSelfBtn) chatTargetSelfBtn.addEventListener("click", () => { if (chatTo) chatTo.value = aiwkDefaultChatTarget("self"); showToast("to=현재 client"); });

  const wssConnectBtn = document.getElementById("wssConnectBtn");
  if (wssConnectBtn) wssConnectBtn.addEventListener("click", async () => { try { const res = await sendCommControl("wss_connect"); setInlineNotice("WSS 연결 시도 완료. 실제 성공은 Ping/Pong 송수신✅ 기준입니다.", "info"); showToast("WSS 연결 시도: " + (res && res.status || "OK")); } catch(e) { showToast("WSS 연결 실패: " + e.message, true); } });
  const wssDisconnectBtn = document.getElementById("wssDisconnectBtn");
  if (wssDisconnectBtn) wssDisconnectBtn.addEventListener("click", async () => { try { const res = await sendCommControl("wss_disconnect"); showToast("WSS 끊기 완료"); } catch(e) { showToast("WSS 끊기 실패: " + e.message, true); } });
  const busConnectBtn = document.getElementById("busConnectBtn");
  if (busConnectBtn) busConnectBtn.addEventListener("click", async () => { try { const res = await sendCommControl("bus_connect"); showToast("BUS 등록: " + (res && res.ok ? "OK" : "확인 필요")); } catch(e) { showToast("BUS 등록 실패: " + e.message, true); } });
  const busDisconnectBtn = document.getElementById("busDisconnectBtn");
  if (busDisconnectBtn) busDisconnectBtn.addEventListener("click", async () => { try { await sendCommControl("bus_disconnect"); showToast("BUS 끊기 완료"); } catch(e) { showToast("BUS 끊기 실패: " + e.message, true); } });
  const nrConnectBtn = document.getElementById("nrConnectBtn");
  if (nrConnectBtn) nrConnectBtn.addEventListener("click", async () => { try { const res = await sendCommControl("nr_connect"); setInlineNotice("NR Ping 전송. N1 pong/chat_ack 수신 시 NR:송수신✅로 표시됩니다.", "info"); showToast("NR Ping 전송: " + (res && res.ok ? "OK" : "확인 필요")); } catch(e) { showToast("NR Ping 실패: " + e.message, true); } });
  const nrDisconnectBtn = document.getElementById("nrDisconnectBtn");
  if (nrDisconnectBtn) nrDisconnectBtn.addEventListener("click", async () => { try { await sendCommControl("nr_disconnect"); showToast("NR 끊기 완료"); } catch(e) { showToast("NR 끊기 실패: " + e.message, true); } });
  const wssKeepChk = document.getElementById("wssKeepChk");
  if (wssKeepChk) wssKeepChk.addEventListener("change", async () => { try { const sec = retrySecValue(); await sendCommControl("wss_keep", { checked:wssKeepChk.checked, seconds:sec, interval_ms:sec * 1000 }); setInlineNotice("WSS 접속유지 " + (wssKeepChk.checked ? "ON · " + sec + "초 간격 재시도" : "OFF"), "info"); showToast("WSS 접속유지 " + (wssKeepChk.checked ? "ON / " + sec + "초" : "OFF")); } catch(e) { showToast("WSS 접속유지 실패: " + e.message, true); } });
  const busKeepChk = document.getElementById("busKeepChk");
  if (busKeepChk) busKeepChk.addEventListener("change", async () => { try { const sec = busSecValue(); await sendCommControl("bus_keep", { checked:busKeepChk.checked, seconds:sec, interval_ms:sec * 1000 }); showToast("BUS 접속유지 " + (busKeepChk.checked ? "ON / " + sec + "초" : "OFF")); } catch(e) { showToast("BUS 접속유지 실패: " + e.message, true); } });
  const nrKeepChk = document.getElementById("nrKeepChk");
  if (nrKeepChk) nrKeepChk.addEventListener("change", async () => { try { const sec = retrySecValue(); await sendCommControl("nr_keep", { checked:nrKeepChk.checked, seconds:sec, interval_ms:sec * 1000 }); setInlineNotice("NR 접속유지 " + (nrKeepChk.checked ? "ON · " + sec + "초 간격으로 N1 Ping" : "OFF"), "info"); showToast("NR 접속유지 " + (nrKeepChk.checked ? "ON / " + sec + "초" : "OFF")); } catch(e) { showToast("NR 접속유지 실패: " + e.message, true); } });
  const wssRetrySec = document.getElementById("wssRetrySec");
  const nrRetrySec = document.getElementById("nrRetrySec");
  const busRetrySec = document.getElementById("busRetrySec");
  async function updateRetryIntervalFromInput(src) {
    const sec = syncRetrySecInputs(src && src.value);
    try {
      await sendCommControl("retry_interval", { seconds:sec, interval_ms:sec * 1000 });
      setInlineNotice("자동 재시도 간격: " + sec + "초. 서버/네트워크 장애 재시도와 NR 유지 Ping에 적용됩니다.", "info");
      showToast("자동 재시도 간격 " + sec + "초");
    } catch(e) {
      showToast("자동 재시도 간격 저장 실패: " + e.message, true);
    }
  }
  if (wssRetrySec) wssRetrySec.addEventListener("change", () => updateRetryIntervalFromInput(wssRetrySec));
  if (nrRetrySec) nrRetrySec.addEventListener("change", () => updateRetryIntervalFromInput(nrRetrySec));
  async function updateBusIntervalFromInput(src) {
    const sec = syncBusSecInput(src && src.value);
    try {
      await sendCommControl("bus_interval", { seconds:sec, interval_ms:sec * 1000 });
      setInlineNotice("BUS 유지 간격: " + sec + "초. WSS/NR 재시도 간격과 별도입니다.", "info");
      showToast("BUS 유지 간격 " + sec + "초");
    } catch(e) {
      showToast("BUS 유지 간격 저장 실패: " + e.message, true);
    }
  }
  if (busRetrySec) busRetrySec.addEventListener("change", () => updateBusIntervalFromInput(busRetrySec));
  const loadDelaySettingsBtn = document.getElementById("loadDelaySettingsBtn");
  const saveDelaySettingsBtn = document.getElementById("saveDelaySettingsBtn");
  const applyDelayToCommBtn = document.getElementById("applyDelayToCommBtn");
  if (loadDelaySettingsBtn) loadDelaySettingsBtn.addEventListener("click", async () => {
    try { const res = await loadCurrentDelaySettings(); showToast("딜레이 불러오기: url5 " + (res.route_key || "-")); }
    catch(e) { showToast("딜레이 불러오기 실패: " + e.message, true); }
  });
  if (saveDelaySettingsBtn) saveDelaySettingsBtn.addEventListener("click", async () => {
    try { const res = await saveCurrentDelaySettings(); showToast("딜레이 저장: url5 " + (res.route_key || "-")); }
    catch(e) { showToast("딜레이 저장 실패: " + e.message, true); }
  });
  if (applyDelayToCommBtn) applyDelayToCommBtn.addEventListener("click", async () => {
    try {
      const res = await saveCurrentDelaySettings();
      const sec = applyDelaySettingsToCommInputs(res.delays);
      await sendCommControl("retry_interval", { seconds:sec, interval_ms:sec * 1000 });
      const busSec = syncBusSecInput((res.delays && res.delays.bus) || 10);
      await sendCommControl("bus_interval", { seconds:busSec, interval_ms:busSec * 1000 });
      setInlineNotice("딜레이 적용: WSS/NR " + sec + "초, BUS " + busSec + "초.", "info");
      showToast("통신 딜레이 적용 WSS/NR " + sec + "초 / BUS " + busSec + "초");
    } catch(e) { showToast("통신 딜레이 적용 실패: " + e.message, true); }
  });
  loadCurrentDelaySettings().catch(()=>{});
  setCommControlsEnabled(commControlsEnabled(cfg));
  refreshCommStatus().catch(()=>{});

  const refreshDebugBtn = document.getElementById("refreshDebugBtn");
  if (refreshDebugBtn) refreshDebugBtn.addEventListener("click", async () => { try { const text = await refreshPopupDebug(); showToast("디버그 갱신 완료 len=" + text.length); } catch(e) { showToast("디버그 갱신 실패: " + e.message, true); } });
  const copyDebugBtn = document.getElementById("copyDebugBtn");
  if (copyDebugBtn) copyDebugBtn.addEventListener("click", async () => { try { const res = await copyPopupDebug(); showToast("디버그 복사 완료 len=" + (res.len || 0)); } catch(e) { showToast("디버그 복사 실패: " + e.message, true); } });
  document.getElementById("saveBtn").addEventListener("click", async () => { try { let cfg = getForm(); cfg = await validateLogin(cfg); await saveConfig(cfg); await saveCurrentDomainTabSettings().catch(()=>{}); showToast("프로필 저장 완료: v152 정본 저장소에 확정했습니다."); } catch(e) { showToast("저장 실패: " + e.message, true); } });
  document.getElementById("openPanelBtn").addEventListener("click", async () => {
    try {
      let cfg = getForm();
      cfg = await validateLogin(cfg);
      await saveConfig(cfg);
      const checked = await requireCurrentTabConfig(cfg, await activeTab());
      const mode = cfg.authMode === "empty" ? "손님" : cfg.authMode + "/" + displayRole(cfg.authRole, cfg.authUserType || (cfg.authPayload && cfg.authPayload.user_type));
      setInlineNotice("📡 WSS 인증등록: " + checked.key + " 확인 완료 → WSS 소켓 연결 → token register 진행 중", "info");
      const res = await sendCommControl("wss_connect", { auth_register: true, force:true });
      setConnected(true);
      setCommControlsEnabled(true);
      await setAuthState({ authenticated:true, connected:true, clientId:cfg.clientId, group:cfg.group, project:cfg.project, roomId:cfg.roomId, authMode:cfg.authMode, authRole:cfg.authRole, authUserType:cfg.authUserType });
      await refreshCommStatus().catch(()=>{});
      showToast("📡 WSS 인증등록 완료: " + mode + " / " + (res && res.status || "OK"));
    } catch(e) {
      await setAuthState({ authenticated:false, connected:false, last_error:e && e.message ? e.message : String(e) });
      showToast("📡 WSS 인증등록 중단: " + (e && e.message ? e.message : String(e)) + "\n구분: 현재 탭 적용 또는 WSS socket/register 단계 오류입니다. 아래쪽 WSS 연결도 같은 경로를 사용합니다.", true);
    }
  });
  const manualBtn = document.getElementById("openManualTestBtn");
  if (manualBtn) manualBtn.addEventListener("click", async () => {
    try {
      let cfg = getForm();
      cfg = await validateLogin(cfg);
      await saveConfig(cfg);
      showToast("수동 테스트 관리창을 엽니다.");
      await openManualTest(cfg);
      setConnected(true);
      showToast("수동 테스트 관리창을 열었습니다.");
    } catch(e) { showToast("수동 테스트 중단: " + e.message, true); }
  });
  document.getElementById("openYyyBtn").addEventListener("click", async () => { const cfg = await saveConfig(); const from = encodeURIComponent(cfg.clientId || ""); await chrome.tabs.create({ url: YYY_AIWK_URL_BASE + "&from=" + from }); });
  document.getElementById("resetPanelBtn").addEventListener("click", async () => { await chrome.storage.local.remove([STORE_PANEL_RECT, STORE_PANEL_MIN, STORE_LAYOUT_EDIT, STORE_YY_DOCK, "AIWK_PANEL_RECT_V1", "AIWK_PANEL_MIN_V1", "AIWK_LAYOUT_EDIT_V1", "AIWK_YY_DOCK_LAYOUT_V1"]); showToast("창 위치 저장값을 초기화했습니다."); });
  bindControlButton("collapseAllBtn", "collapse_all", "전체 1단");
  bindControlButton("collapse2AllBtn", "collapse2_all", "전체 2단");
  bindControlButton("iconAllBtn", "icon_all", "전체 3단");
  bindControlButton("expandAllBtn", "expand_all", "전체 펼침");
  bindControlButton("hideAllBtn", "hide_all", "전체 숨김");
  bindControlButton("showAllBtn", "show_all", "전체 보임");
  const countResetBtn = document.getElementById("aiwkCountResetBtn");
  if (countResetBtn) countResetBtn.addEventListener("click", async () => {
    try {
      const res = await sendCommControl("count_reset");
      await refreshCommStatus().catch(()=>{});
      showToast("송수신 카운트 리셋: " + (res && res.ok ? "OK" : "확인 필요"));
    } catch(e) {
      showToast("송수신 카운트 리셋 실패: " + e.message, true);
    }
  });
  const closeAllBtn = document.getElementById("closeAllBtn");
  if (closeAllBtn) closeAllBtn.addEventListener("click", async () => { try { await shutdownAll(); } catch(e) { showToast("전체 종료 실패: " + e.message, true); } });
  applyPlaceholderTooltips(document);
});
