// /AIWK_EXT4/service_worker.js
// 용도: Chrome extension service worker. popup/relay/content 메시지 중계, Relay 창 focus, domain module loader 보조를 담당한다.
// AIWK EXT4 v152-storage-namespace-profile-lock
(function () {
    "use strict";

    const VERSION = "v152-storage-namespace-profile-lock";
    const REMOTE_TOKEN_URL = "https://i.yjm.kr/yw/conf/token.txt";
    const PROFILE_INSTANCE_KEY = "AIWK_PROFILE_INSTANCE_ID";

    function sleep(ms) {
        return new Promise(function (resolve) { setTimeout(resolve, ms); });
    }

    function makeProfileInstanceId() {
        return "pf_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 10);
    }
    async function getProfileInstanceId() {
        const obj = await chrome.storage.local.get([PROFILE_INSTANCE_KEY]);
        let id = String((obj && obj[PROFILE_INSTANCE_KEY]) || "").trim();
        if (!id) {
            id = makeProfileInstanceId();
            await chrome.storage.local.set({ [PROFILE_INSTANCE_KEY]: id });
        }
        return id;
    }

    async function waitTabComplete(tabId) {
        for (let i = 0; i < 40; i += 1) {
            const tab = await chrome.tabs.get(tabId).catch(function () { return null; });
            if (tab && tab.status === "complete") {
                return tab;
            }
            await sleep(300);
        }
        throw new Error("TOKEN_TAB_LOAD_TIMEOUT: token.txt 탭 로딩 시간이 초과되었습니다.");
    }

    async function readTokenByTab() {
        let tab = null;
        try {
            tab = await chrome.tabs.create({ url: REMOTE_TOKEN_URL, active: false });
            if (!tab || !tab.id) {
                throw new Error("TOKEN_TAB_CREATE_FAILED: token.txt 읽기용 탭을 만들 수 없습니다.");
            }
            await waitTabComplete(tab.id);
            const injected = await chrome.scripting.executeScript({
                target: { tabId: tab.id },
                func: function () {
                    return String((document.body && (document.body.innerText || document.body.textContent)) || "").trim();
                }
            });
            const token = String((injected && injected[0] && injected[0].result) || "").trim();
            if (!token) {
                throw new Error("TOKEN_TAB_EMPTY: token.txt 내용이 비어 있습니다.");
            }
            return {
                ok: 1,
                version: VERSION,
                bus: "service_worker_tabbus",
                method: "tab+scripting",
                url: REMOTE_TOKEN_URL,
                token: token,
                token_len: token.length,
                tab_id: tab.id
            };
        } finally {
            if (tab && tab.id) {
                chrome.tabs.remove(tab.id).catch(function () {});
            }
        }
    }



    const CONTENT_SCRIPT_FILES = [
        "AIWK_COMMON/core/aiwk_core.js",
        "AIWK_COMMON/core/aiwk_state.js",
        "AIWK_COMMON/core/aiwk_event.js",
        "AIWK_COMMON/core/aiwk_store.js",
        "AIWK_COMMON/core/aiwk_module.js",
        "AIWK_COMMON/core/aiwk_i18n.js",
        "AIWK_COMMON/core/aiwk_auth.js",
        "AIWK_COMMON/core/aiwk_bus_graph.js",
        "AIWK_COMMON/core/aiwk_url_context.js",
        "AIWK_COMMON/core/aiwk_comm.js",
        "AIWK_COMMON/core/aiwk_protocol.js",
        "AIWK_COMMON/action_ui/aiwk_action_ui_basic.js",
        "AIWK_COMMON/core/yw-diagnostic.js",
        "AIWK_COMMON/core/yw-module-install.js",
        "AIWK_COMMON/core/yw-domain-loader.js",
        "AIWK_COMMON/core/yw-route.js",
        "AIWK_COMMON/core/yw-window-state.js",
        "AIWK_COMMON/loader/aiwk_asset_loader.js",
        "AIWK_COMMON/loader/aiwk_css_loader.js",
        "AIWK_COMMON/loader/aiwk_json_loader.js",
        "AIWK_COMMON/loader/aiwk_script_loader.js",
        "AIWK_COMMON/loader/aiwk_loader.js",
        "AIWK_COMMON/win/aiwk_win_render.js",
        "AIWK_COMMON/win/aiwk_win_drag.js",
        "AIWK_COMMON/win/aiwk_win_resize.js",
        "AIWK_COMMON/win/aiwk_win_menu.js",
        "AIWK_COMMON/win/aiwk_win_class.js",
        "AIWK_COMMON/win/aiwk_win.js",
        "AIWK_COMMON/group/aiwk_group.js",
        "AIWK_COMMON/group/aiwk_zone_class.js",
        "AIWK_COMMON/site/aiwk_site_profile.js",
        "AIWK_COMMON/group/aiwk_seg.js",
        "AIWK_COMMON/modules/sample/hello.js",
        "AIWK_COMMON/modules/auth/panel.js",
        "AIWK_COMMON/modules/win/registry.js",
        "AIWK_COMMON/modules/zone/registry.js",
        "AIWK_COMMON/modules/site/profile_registry.js",
        "AIWK_COMMON/modules/dev/js_runner.js",
        "AIWK_COMMON/modules/dev/test_launcher.js",
        "AIWK_COMMON/modules/dev/manual_test_manager.js",
        "AIWK_COMMON/modules/comm/bus_test.js",
        "AIWK_COMMON/modules/comm/wss_test.js",
        "AIWK_COMMON/modules/comm/nr_test.js",
        "AIWK_EXT4/content.js",
        "AIWK_EXT4/aiwk_uri_runner.js"
    ];

    const FRAME_BRIDGE_FILES = [
        "AIWK_COMMON/modules/core/window_controls.js",
        "AIWK_EXT4/frame_bridge.js"
    ];

    function canInjectIntoTab(tab) {
        const url = String((tab && tab.url) || "");
        if (!tab || !tab.id) return false;
        if (/^(chrome|edge|brave|about|devtools):/i.test(url)) return false;
        if (/^chrome-extension:/i.test(url)) return false;
        return true;
    }

    function routeConfigKey(routeKey) {
        return "AIWK_ROUTE_CONFIG:" + String(routeKey || "");
    }
    function mergeRouteTabState(cfg, tab, patch) {
        cfg = Object.assign({}, cfg || {});
        const tabId = String(tab && tab.id || patch && patch.tab_id || "");
        if (!tabId) return cfg;
        const tabs = Object.assign({}, cfg.tabs || {});
        const oldTab = Object.assign({}, tabs[tabId] || {});
        tabs[tabId] = Object.assign({}, oldTab, patch || {}, {
            tab_id: tabId,
            title: tab && tab.title || oldTab.title || "",
            url: tab && tab.url || oldTab.url || "",
            window_id: tab && tab.windowId || oldTab.window_id || "",
            updated_at: new Date().toISOString()
        });
        cfg.tabs = tabs;
        cfg.tab_id = tabId;
        cfg.title = tab && tab.title || cfg.title || "";
        cfg.url = tab && tab.url || cfg.url || "";
        cfg.updated_at = new Date().toISOString();
        return cfg;
    }

    async function saveScopedTabConfig(tab, config) {
        if (!tab || !tab.id) return { ok: 0, reason: "tab_missing" };
        const profileInstanceId = await getProfileInstanceId();
        const routeKey = String(config && (config.route_key || config.url_key) || "");
        const routeStoreKey = routeConfigKey(routeKey);
        const oldObj = routeKey ? await chrome.storage.local.get([routeStoreKey]).catch(function () { return {}; }) : {};
        const oldCfg = routeKey ? (oldObj && oldObj[routeStoreKey] || {}) : {};
        let cfg = Object.assign({}, oldCfg, config || {}, {
            tab_id: String(tab.id),
            route_key: routeKey,
            url_key: String(config && (config.url_key || config.route_key) || routeKey),
            profile_instance_id: profileInstanceId,
            profileInstanceId: profileInstanceId,
            title: tab.title || "",
            url: tab.url || "",
            updated_at: new Date().toISOString(),
            source: "service_worker.openAuthOnActiveTab"
        });
        cfg = mergeRouteTabState(cfg, tab, { enabled: true, status: "ACTIVE", source: "service_worker.openAuthOnActiveTab" });
        await chrome.storage.local.set({ [routeConfigKey(cfg.route_key || cfg.url_key)]: cfg });
        return { ok: 1, tab_id: String(tab.id), config: cfg };
    }

    async function sendMessageToTab(tabId, msg) {
        try {
            return await chrome.tabs.sendMessage(tabId, msg);
        } catch (err) {
            const em = err && err.message ? err.message : String(err);
            return { ok: 0, version: VERSION, code: runtimeMessageCode(em), error: em, tab_id: String(tabId || "") };
        }
    }

    function runtimeMessageCode(text) {
        text = String(text || "");
        if (/Receiving end does not exist|Could not establish connection/i.test(text)) return "RECEIVING_END_MISSING";
        if (/message channel closed|message port closed/i.test(text)) return "MESSAGE_CHANNEL_CLOSED";
        if (/Extension context invalidated/i.test(text)) return "EXTENSION_CONTEXT_INVALIDATED";
        return "RUNTIME_MESSAGE_FAIL";
    }

    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 getActiveUsableTab() {
        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;
        for (const w of wins || []) {
            const t = (w.tabs || []).find(x => x.active && isUsablePageTab(x));
            if (t) return t;
        }
        for (const w of wins || []) {
            const t = (w.tabs || []).find(x => isUsablePageTab(x));
            if (t) return t;
        }
        return null;
    }

    async function ensureContentScripts(tab) {
        if (!canInjectIntoTab(tab)) {
            throw new Error("TAB_NOT_INJECTABLE: 이 탭에는 EXT 내용을 주입할 수 없습니다. 일반 웹페이지나 ChatGPT 탭을 선택한 뒤 아이콘을 누르십시오.");
        }
        await chrome.scripting.executeScript({
            target: { tabId: tab.id },
            files: CONTENT_SCRIPT_FILES
        });
        await sleep(300);
    }

    async function openAuthOnActiveTab(config) {
        const tab = await getActiveUsableTab();
        if (!tab || !tab.id) {
            throw new Error("ACTIVE_TAB_NOT_FOUND: 현재 활성 일반 웹 탭을 찾을 수 없습니다.");
        }
        try {
            await saveScopedTabConfig(tab, config || {});
            return await sendMessageToTab(tab.id, { type: "AIWK_EXT4_OPEN_AUTH", config: config || {} });
        } catch (err) {
            const msg = err && err.message ? err.message : String(err);
            if (msg.indexOf("Receiving end does not exist") < 0 && msg.indexOf("Could not establish connection") < 0) {
                throw err;
            }
            await ensureContentScripts(tab);
            await saveScopedTabConfig(tab, config || {});
            return await sendMessageToTab(tab.id, { type: "AIWK_EXT4_OPEN_AUTH", config: config || {} });
        }
    }


    async function controlActiveTab(action) {
        const tab = await getActiveUsableTab();
        if (!tab || !tab.id) {
            throw new Error("ACTIVE_TAB_NOT_FOUND: 현재 활성 일반 웹 탭을 찾을 수 없습니다.");
        }
        if (action === "close_all") {
            const disposed = await disposeAiwkFrames(tab, "user_closed");
            let topRes = null;
            try { topRes = await sendMessageToTab(tab.id, { type: "AIWK_EXT4_WIN_CONTROL", action: action, status: "USER_CLOSED" }); }
            catch (err) { topRes = { ok: 0, error: err && err.message ? err.message : String(err) }; }
            return { ok: 1, version: VERSION, action: "close_all", status: "USER_CLOSED", emoji: "⏹", disposed: disposed, top: topRes };
        }
        try {
            return await sendMessageToTab(tab.id, { type: "AIWK_EXT4_WIN_CONTROL", action: action });
        } catch (err) {
            const msg = err && err.message ? err.message : String(err);
            if (msg.indexOf("Receiving end does not exist") < 0 && msg.indexOf("Could not establish connection") < 0) {
                throw err;
            }
            await ensureContentScripts(tab);
            return await sendMessageToTab(tab.id, { type: "AIWK_EXT4_WIN_CONTROL", action: action });
        }
    }

    async function disposeAiwkFrames(tab, reason) {
        if (!tab || !tab.id) return { ok: 0, code: "TAB_MISSING" };
        const frames = await chrome.webNavigation.getAllFrames({ tabId: tab.id }).catch(function () { return []; });
        const results = [];
        for (const f of frames || []) {
            try {
                const res = await chrome.tabs.sendMessage(tab.id, { type: "AIWK_FRAME_DISPOSE_ALL", reason: reason || "user_closed" }, { frameId: Number(f.frameId) });
                results.push({ ok: 1, frame_id: Number(f.frameId), res: res || {} });
            } catch (err) {
                results.push({ ok: 0, frame_id: Number(f.frameId), error: err && err.message ? err.message : String(err) });
            }
        }
        return { ok: 1, version: VERSION, action: "dispose_aiwk_frames", count: results.length, results: results.slice(0, 20) };
    }

    function replyError(sendResponse, code, detail, extra) {
        sendResponse(Object.assign({
            ok: 0,
            version: VERSION,
            bus: "service_worker_tabbus",
            url: REMOTE_TOKEN_URL,
            code: code,
            error: detail || code,
            fix: "tabs/scripting 권한, https://i.yjm.kr/* host_permissions, token.txt 경로를 확인하십시오."
        }, extra || {}));
    }





    // v070 Relay Panel runtime state: packet flow log, tab context and broadcast controls.
    const AIWK_RELAY_LOG = [];
    const AIWK_RELAY_TAB_STATE = {};
    const AIWK_RELAY_MAX_LOG = 500;
    let AIWK_RELAY_ACTIVE_BROWSER_TAB_ID = "";
    let AIWK_RELAY_WINDOW_ID = 0;
    let AIWK_MODULE_REGISTRY_CACHE = null;

    function relayNow() { return new Date().toISOString(); }
    function relayPush(row) {
        row = Object.assign({ at: relayNow(), ts: Date.now() }, row || {});
        AIWK_RELAY_LOG.push(row);
        while (AIWK_RELAY_LOG.length > AIWK_RELAY_MAX_LOG) AIWK_RELAY_LOG.shift();
        try { chrome.runtime.sendMessage({ type: "AIWK_RELAY_STATE_CHANGED", row: row }).catch(function () {}); } catch (err) {}
        return row;
    }

    function relayTabKey(tabId) { return String(tabId || ""); }
    function findRouteConfigForTab(tab, allStore) {
        const tabId = String(tab && tab.id || "");
        if (!tabId) return { route_key: "", config: {} };
        const prefix = "AIWK_ROUTE_CONFIG:";
        for (const key of Object.keys(allStore || {})) {
            if (key.indexOf(prefix) !== 0) continue;
            const cfg = allStore[key] || {};
            if (cfg.tabs && cfg.tabs[tabId]) return { route_key: key.slice(prefix.length), config: cfg };
        }
        for (const key of Object.keys(allStore || {})) {
            if (key.indexOf(prefix) !== 0) continue;
            const cfg = allStore[key] || {};
            if (String(cfg.url || "") && String(cfg.url || "") === String(tab && tab.url || "")) return { route_key: key.slice(prefix.length), config: cfg };
        }
        return { route_key: "", config: {} };
    }

    async function relayListTabs() {
        const tabs = await chrome.tabs.query({});
        const allStore = await chrome.storage.local.get(null).catch(function () { return {}; });
        return (tabs || []).filter(function (tab) { return tab && tab.id && !/^(chrome|edge|brave|about|devtools|chrome-extension):/i.test(String(tab.url || "")); }).map(function (tab) {
            const saved = AIWK_RELAY_TAB_STATE[relayTabKey(tab.id)] || {};
            let savedRoute = saved.route_key || saved.url_key || (saved.comm && saved.comm.route && (saved.comm.route.route_key || saved.comm.route.url_key)) || "";
            const foundRoute = savedRoute ? { route_key: savedRoute, config: allStore[routeConfigKey(savedRoute)] || {} } : findRouteConfigForTab(tab, allStore);
            savedRoute = savedRoute || foundRoute.route_key || "";
            const cfg = foundRoute.config || {};
            const tabState = cfg.tabs && cfg.tabs[String(tab.id)] ? cfg.tabs[String(tab.id)] : {};
            const ended = tabState.enabled === false || !!tabState.ended || String(tabState.status || "").toUpperCase() === "END";
            const identity = {
                client_id: cfg.clientId || cfg.client_id || saved.client_id,
                profile_instance_id: cfg.profileInstanceId || cfg.profile_instance_id || saved.profile_instance_id,
                route_key: savedRoute || saved.route_key || "",
                url_key: savedRoute || saved.url_key || "",
                status: ended ? "USER_CLOSED" : (cfg.status || saved.status),
                aiwk_status: cfg.aiwk_status || cfg.status || saved.aiwk_status,
                ended: ended,
                route_enabled: tabState.enabled !== false,
                user_closed: ended ? 1 : 0,
                end_label: cfg.end_label || ""
            };
            return Object.assign({ tab_id: tab.id, title: tab.title || "", url: tab.url || "", active: !!tab.active, window_id: tab.windowId }, saved || {}, identity);
        });
    }

    async function relaySnapshot() {
        const tabs = await relayListTabs();
        const actualTab = await getActiveUsableTab().catch(function () { return null; });
        const actualId = actualTab && actualTab.id ? String(actualTab.id) : "";
        if (actualId) AIWK_RELAY_ACTIVE_BROWSER_TAB_ID = actualId;
        const selectedId = String(AIWK_RELAY_ACTIVE_BROWSER_TAB_ID || "");
        const selected = selectedId ? tabs.find(function (t) { return String(t.tab_id || "") === selectedId; }) : null;
        const actual = actualId ? tabs.find(function (t) { return String(t.tab_id || "") === actualId; }) : null;
        const active = actual || selected || tabs.find(function (t) { return t.active; }) || tabs[0] || null;
        return { ok: 1, version: VERSION, at: relayNow(), selected_tab_id: selectedId, active_tab: active, tabs: tabs, log: AIWK_RELAY_LOG.slice(-200), tab_state: AIWK_RELAY_TAB_STATE };
    }

    async function relayActivateTab(tabId) {
        const id = Number(tabId || 0);
        if (!id) return { ok: 0, version: VERSION, code: "TAB_ID_MISSING", error: "tab_id is required" };
        let tab = null;
        try { tab = await chrome.tabs.get(id); } catch (err) { tab = null; }
        if (!tab || !tab.id) return { ok: 0, version: VERSION, code: "TARGET_TAB_NOT_FOUND", error: "tab not found: " + id };
        AIWK_RELAY_ACTIVE_BROWSER_TAB_ID = String(tab.id);
        if (tab.windowId) await chrome.windows.update(tab.windowId, { focused: true });
        await chrome.tabs.update(tab.id, { active: true });
        relayPush({ dir: "tab", action: "activate_request", tab_id: tab.id, window_id: tab.windowId || "", title: tab.title || "", url: tab.url || "" });
        return { ok: 1, version: VERSION, tab_id: String(tab.id), window_id: tab.windowId || 0 };
    }

    async function relayBroadcast(message) {
        const tabs = await chrome.tabs.query({});
        const results = [];
        await Promise.all((tabs || []).map(async function (tab) {
            if (!canInjectIntoTab(tab)) return;
            try {
                const res = await chrome.tabs.sendMessage(tab.id, message || {});
                results.push({ ok: 1, tab_id: tab.id, title: tab.title || "", res: res || {} });
            } catch (err) {
                results.push({ ok: 0, tab_id: tab.id, title: tab.title || "", error: err && err.message ? err.message : String(err) });
            }
        }));
        relayPush({ dir: "control", action: "broadcast", message_type: message && message.type || "", count: results.length, results: results.slice(0, 20) });
        return { ok: 1, version: VERSION, count: results.length, results: results };
    }

    async function relayOpenPopup() {
        const relayUrl = chrome.runtime.getURL("AIWK_RELAY/side_panel.html?mode=popup");
        const baseUrl = chrome.runtime.getURL("AIWK_RELAY/side_panel.html");
        async function focusRelayWindow(win, tab, method) {
            if (!win || !win.id) return { ok: 0, version: VERSION, mode: "popup", code: "RELAY_WINDOW_ID_MISSING", method: method || "" };
            AIWK_RELAY_WINDOW_ID = win.id || 0;
            try { await chrome.windows.update(win.id, { state: "normal", focused: true }); }
            catch (e1) { await chrome.windows.update(win.id, { focused: true }); }
            if (tab && tab.id) await chrome.tabs.update(tab.id, { active: true });
            return { ok: 1, version: VERSION, mode: "popup", method: method || "focus_existing", window_id: win.id || 0, tab_id: tab && tab.id || 0 };
        }
        try {
            await chrome.storage.local.set({ AIWK_RELAY_MODE: "popup" });
            if (AIWK_RELAY_WINDOW_ID) {
                try {
                    const w0 = await chrome.windows.get(AIWK_RELAY_WINDOW_ID, { populate: true });
                    const t0 = (w0.tabs || []).find(function (t) {
                        const u = String(t.url || "");
                        return u.indexOf(baseUrl) === 0 && u.indexOf("mode=sidepanel") < 0;
                    });
                    if (t0) return await focusRelayWindow(w0, t0, "window_id.focus_existing");
                    AIWK_RELAY_WINDOW_ID = 0;
                } catch (e) {
                    AIWK_RELAY_WINDOW_ID = 0;
                }
            }
            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.indexOf(baseUrl) === 0 && u.indexOf("mode=sidepanel") < 0) {
                        return await focusRelayWindow(w, t, "scan.focus_existing");
                    }
                }
            }
            let cur = null;
            try { cur = await chrome.windows.getCurrent(); } catch (e) { cur = null; }
            const width = 560;
            const height = 820;
            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: width, height: height, focused: true };
            if (left !== undefined) opt.left = left;
            if (top !== undefined) opt.top = top;
            const win = await chrome.windows.create(opt);
            AIWK_RELAY_WINDOW_ID = win && win.id ? win.id : 0;
            return { ok: 1, version: VERSION, mode: "popup", method: "windows.create", window_id: AIWK_RELAY_WINDOW_ID };
        } catch (err) {
            return { ok: 0, version: VERSION, mode: "popup", code: "RELAY_POPUP_OPEN_FAIL", error: err && err.message ? err.message : String(err) };
        }
    }

    async function relayClosePopup() {
        if (!AIWK_RELAY_WINDOW_ID) return { ok: 1, version: VERSION, closed: 0 };
        const id = AIWK_RELAY_WINDOW_ID;
        AIWK_RELAY_WINDOW_ID = 0;
        try { await chrome.windows.remove(id); return { ok: 1, version: VERSION, closed: 1, window_id: id }; }
        catch (err) { return { ok: 0, version: VERSION, closed: 0, window_id: id, error: err && err.message ? err.message : String(err) }; }
    }

    async function relayOpenSidePanel() {
        try {
            await chrome.storage.local.set({ AIWK_RELAY_MODE: "sidepanel" });
            if (!chrome.sidePanel || !chrome.sidePanel.open) {
                return { ok: 0, version: VERSION, mode: "sidepanel", code: "SIDE_PANEL_API_MISSING", error: "chrome.sidePanel API가 없습니다." };
            }
            await chrome.sidePanel.setOptions({ path: "AIWK_RELAY/side_panel.html?mode=sidepanel", enabled: true });
            const win = await chrome.windows.getCurrent().catch(function(){ return null; });
            const windowId = win && win.id ? win.id : undefined;
            await chrome.sidePanel.open({ windowId: windowId });
            return { ok: 1, version: VERSION, mode: "sidepanel", method: "sidePanel.open", window_id: windowId || 0 };
        } catch (err) {
            return { ok: 0, version: VERSION, mode: "sidepanel", code: "SIDE_PANEL_OPEN_FAIL", error: err && err.message ? err.message : String(err) };
        }
    }

    async function relayOpenPanel() {
        return await relayOpenPopup();
    }


    const AIWK_TAB_BUS_REGISTRY = {};
    function aiwkTabRoomKey(profile) {
        profile = profile || {};
        return [profile.group || "home", profile.project || "IG_CRM", profile.roomId || profile.room || ""].join(".");
    }
    function aiwkClientPatternMatch(clientId, pattern) {
        clientId = String(clientId || "");
        pattern = String(pattern || "");
        if (!pattern || pattern === "*") return true;
        if (pattern.endsWith("*")) return clientId.indexOf(pattern.slice(0, -1)) === 0;
        return clientId === pattern;
    }
    function aiwkParseTo(to) {
        let t = String(to || "").trim();
        let endpoint = "";
        const h = t.indexOf("#");
        if (h >= 0) { endpoint = t.slice(h + 1); t = t.slice(0, h); }
        const parts = t.split(".").filter(Boolean);
        let client = parts.length >= 1 ? parts[parts.length - 1] : t;
        if (client.indexOf(":") >= 0) client = client.split(":")[0];
        return { raw: String(to || ""), client: client || "*", endpoint: endpoint };
    }
    function aiwkTabRegistryList(profile) {
        const key = aiwkTabRoomKey(profile);
        const now = Date.now();
        const list = Object.keys(AIWK_TAB_BUS_REGISTRY).map(k => AIWK_TAB_BUS_REGISTRY[k]).filter(row => row.room_key === key && now - row.last_seen < 60000);
        return list.sort((a,b) => String(a.client_id).localeCompare(String(b.client_id)) || String(a.endpoint).localeCompare(String(b.endpoint)));
    }

    chrome.runtime.onInstalled.addListener(function () {
        chrome.storage.local.set({ aiwk_ext4_version: VERSION });
    });

    chrome.tabs.onActivated.addListener(function (activeInfo) {
        const tabId = activeInfo && activeInfo.tabId || "";
        if (tabId) {
            chrome.tabs.get(tabId).then(function (tab) {
                if (isUsablePageTab(tab)) AIWK_RELAY_ACTIVE_BROWSER_TAB_ID = String(tabId);
            }).catch(function () {});
        }
        relayPush({ dir: "tab", action: "activated", tab_id: tabId, window_id: activeInfo && activeInfo.windowId || "" });
    });
    chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
        if (changeInfo && (changeInfo.url || changeInfo.title || changeInfo.status === "complete")) {
            const key = relayTabKey(tabId);
            AIWK_RELAY_TAB_STATE[key] = Object.assign({}, AIWK_RELAY_TAB_STATE[key] || {}, { tab_id: tabId, title: tab && tab.title || "", url: tab && tab.url || "", last_seen: Date.now() });
            relayPush({ dir: "tab", action: "updated", tab_id: tabId, title: tab && tab.title || "", url: tab && tab.url || "", status: changeInfo.status || "" });
        }
    });

    async function verifyRemoteToken(inputToken) {
        const token = String(inputToken || "").trim();
        if (!/^[a-f0-9]{64}$/i.test(token)) {
            throw new Error("TOKEN_FORMAT_INVALID: 서버 토큰은 64자리 hex 문자열이어야 합니다.");
        }
        const res = await readTokenByTab();
        const serverToken = String((res && res.token) || "").trim();
        if (!serverToken) {
            throw new Error("REMOTE_TOKEN_EMPTY: 서버 token.txt가 비어 있습니다.");
        }
        if (serverToken !== token) {
            throw new Error("TOKEN_MISMATCH: 입력 token과 서버 token.txt 값이 다릅니다.");
        }
        return Object.assign({}, res, { token: "", verified: 1, token_len: token.length });
    }


    async function findWeb1TabAndDeliver(report) {
        const tabs = await chrome.tabs.query({});
        const candidates = (tabs || []).filter(function (tab) {
            const url = String((tab && tab.url) || "");
            return tab && tab.id && /https:\/\/home\.yjm\.kr\//i.test(url) && /AIWK_WEB|\/aiwk\//i.test(url);
        });
        if (!candidates.length) {
            return { ok: 0, version: VERSION, code: "WEB1_TAB_NOT_FOUND", message: "home.yjm.kr/aiwk WEB1 탭을 먼저 열어야 합니다." };
        }
        const tab = candidates.find(function (t) { return t.active; }) || candidates[0];
        try {
            return await chrome.tabs.sendMessage(tab.id, { type: "AIWK_WEB1_RECEIVE_DIAG", report: report || {}, from: "EXT4", version: VERSION });
        } catch (err) {
            const msg = err && err.message ? err.message : String(err);
            if (msg.indexOf("Receiving end does not exist") < 0 && msg.indexOf("Could not establish connection") < 0) {
                return { ok: 0, version: VERSION, code: "WEB1_DELIVER_FAIL", tab_id: tab.id, error: msg };
            }
            await ensureContentScripts(tab);
            return await chrome.tabs.sendMessage(tab.id, { type: "AIWK_WEB1_RECEIVE_DIAG", report: report || {}, from: "EXT4", version: VERSION });
        }
    }

    async function findChatGptTabAndInsert(text) {
        const tabs = await chrome.tabs.query({});
        const candidates = (tabs || []).filter(function (tab) {
            const url = String((tab && tab.url) || "");
            return tab && tab.id && /https:\/\/(chatgpt\.com|chat\.openai\.com)\//i.test(url);
        });
        if (!candidates.length) {
            return { ok: 0, version: VERSION, code: "CHATGPT_TAB_NOT_FOUND", message: "ChatGPT 탭을 먼저 열어야 합니다." };
        }
        const tab = candidates.find(function (t) { return t.active; }) || candidates[0];
        try {
            return await chrome.tabs.sendMessage(tab.id, { type: "AIWK_CHATGPT_INSERT_IN_TAB", text: String(text || "") });
        } catch (err) {
            const msg = err && err.message ? err.message : String(err);
            if (msg.indexOf("Receiving end does not exist") < 0 && msg.indexOf("Could not establish connection") < 0) {
                return { ok: 0, version: VERSION, code: "CHATGPT_INSERT_FAIL", tab_id: tab.id, error: msg };
            }
            await ensureContentScripts(tab);
            return await chrome.tabs.sendMessage(tab.id, { type: "AIWK_CHATGPT_INSERT_IN_TAB", text: String(text || "") });
        }
    }



    async function sendActiveTabWithEnsure(message, explicitTabId) {
        let tab = null;
        const wantId = explicitTabId || (message && message.target_tab_id) || "";
        if (wantId) {
            try { tab = await chrome.tabs.get(Number(wantId)); } catch (e) { tab = null; }
            if (!tab || !tab.id) return { ok: 0, version: VERSION, code: "TARGET_TAB_NOT_FOUND", error: "target_tab_id not found: " + wantId };
        } else {
            const tabs = await chrome.tabs.query({ active: true, currentWindow: true }).catch(function(){ return []; });
            tab = tabs && tabs[0] ? tabs[0] : null;
        }
        if (!tab || !tab.id) return { ok: 0, version: VERSION, code: "ACTIVE_TAB_NOT_FOUND", error: "active tab not found" };
        if (!isUsablePageTab(tab)) {
            return { ok: 0, version: VERSION, code: "TARGET_TAB_NOT_USABLE", error: "지원불가 페이지입니다.", tab_id: String(tab.id), url: tab.url || "", solution: "chrome://newtab, chrome://extensions 같은 브라우저 내부 페이지에는 content script 메시지를 보내지 않습니다." };
        }
        const payload = Object.assign({}, message || {}, { target_tab_id:String(tab.id) });
        let res = await sendMessageToTab(tab.id, payload);
        if (res && res.ok === 0 && /RECEIVING_END_MISSING|RUNTIME_MESSAGE_FAIL/i.test(String(res.code || ""))) {
            await ensureContentScripts(tab).catch(function () {});
            res = await sendMessageToTab(tab.id, payload);
        }
        return res;
    }


    async function getTargetTabByMessage(msg) {
        let tab = null;
        const wantId = String((msg && (msg.target_tab_id || msg.tab_id || msg.tabId)) || "").trim();
        if (wantId) {
            tab = await chrome.tabs.get(Number(wantId)).catch(function () { return null; });
        }
        if (!tab || !tab.id) {
            tab = await getActiveUsableTab();
        }
        if (!tab || !tab.id) throw new Error("TARGET_TAB_NOT_FOUND");
        if (!isUsablePageTab(tab)) throw new Error("TARGET_TAB_NOT_USABLE: tab_id=" + tab.id + " url=" + (tab.url || ""));
        return tab;
    }

    async function ensureFrameBridge(tab) {
        if (!canInjectIntoTab(tab)) throw new Error("TAB_NOT_INJECTABLE_FOR_FRAME_BRIDGE: " + (tab && tab.url || ""));
        await chrome.scripting.executeScript({
            target: { tabId: tab.id, allFrames: true },
            files: FRAME_BRIDGE_FILES
        }).catch(function (err) {
            const msg = err && err.message ? err.message : String(err);
            if (msg.indexOf("Cannot access") >= 0 || msg.indexOf("No matching signature") >= 0) throw err;
        });
        return { ok: 1, tab_id: String(tab.id) };
    }

    async function aiwkSelectorListFrames(msg) {
        const tab = await getTargetTabByMessage(msg || {});
        await ensureFrameBridge(tab);
        const iframeRows = await chrome.scripting.executeScript({
            target: { tabId: tab.id },
            func: function () {
                return Array.from(document.querySelectorAll("iframe")).map(function (f, i) {
                    return { index: i, id: f.id || "", name: f.name || "", className: String(f.className || ""), src: f.src || f.getAttribute("src") || "", title: f.title || f.getAttribute("title") || "" };
                });
            }
        }).catch(function () { return []; });
        const topIframes = iframeRows && iframeRows[0] && iframeRows[0].result ? iframeRows[0].result : [];
        const injected = await chrome.scripting.executeScript({
            target: { tabId: tab.id, allFrames: true },
            func: function () {
                return { is_top: window.top === window, href: location.href, origin: location.origin, title: document.title || "", referrer: document.referrer || "", ready_state: document.readyState || "" };
            }
        });
        const frames = (injected || []).map(function (row, idx) {
            const r = row.result || {};
            return Object.assign({ frame_index: idx, frame_id: row.frameId, document_id: row.documentId || "" }, r);
        });
        return { ok: 1, version: VERSION, tab_id: String(tab.id), tab: { title: tab.title || "", url: tab.url || "" }, iframe_count: topIframes.length, iframes: topIframes, frame_count: frames.length, frames: frames };
    }

    async function resolveFrameForSelector(tab, req) {
        req = req || {};
        const wantedFrameId = req.frame_id !== undefined && req.frame_id !== null && String(req.frame_id) !== "" ? Number(req.frame_id) : null;
        const wantedIndex = req.frame_index !== undefined && req.frame_index !== null && String(req.frame_index) !== "" ? Number(req.frame_index) : null;
        const urlContains = String(req.frame_url_contains || req.url_contains || (req.frame && (req.frame.url_contains || req.frame.href_contains)) || "").trim();
        const mode = String(req.frame_mode || (req.frame && req.frame.mode) || "").trim().toLowerCase();
        const iframeSelector = String(req.iframe_selector || (req.frame && req.frame.selector) || "").trim();
        let iframeSrc = "";
        if (iframeSelector) {
            const rows = await chrome.scripting.executeScript({
                target: { tabId: tab.id },
                args: [iframeSelector],
                func: function (sel) {
                    const f = document.querySelector(sel);
                    if (!f) return { ok: 0, code: "IFRAME_SELECTOR_NOT_FOUND", selector: sel };
                    return { ok: 1, selector: sel, src: f.src || f.getAttribute("src") || "", id: f.id || "", name: f.name || "", title: f.title || "" };
                }
            });
            const r = rows && rows[0] ? rows[0].result : null;
            if (!r || !r.ok) throw new Error("IFRAME_SELECTOR_NOT_FOUND: " + iframeSelector);
            iframeSrc = String(r.src || "");
        }
        const listed = await aiwkSelectorListFrames({ target_tab_id: tab.id });
        let frames = listed.frames || [];
        if (wantedFrameId !== null && !Number.isNaN(wantedFrameId)) {
            const found = frames.find(function (f) { return Number(f.frame_id) === wantedFrameId; });
            if (found) return found;
            throw new Error("FRAME_ID_NOT_FOUND: " + wantedFrameId);
        }
        if (wantedIndex !== null && !Number.isNaN(wantedIndex)) {
            const found = frames.find(function (f) { return Number(f.frame_index) === wantedIndex; });
            if (found) return found;
            throw new Error("FRAME_INDEX_NOT_FOUND: " + wantedIndex);
        }
        if (iframeSrc) {
            const matched = frames.filter(function (f) { return String(f.href || "") === iframeSrc || String(f.href || "").indexOf(iframeSrc) >= 0 || iframeSrc.indexOf(String(f.href || "")) >= 0; });
            if (matched.length === 1) return matched[0];
            if (matched.length > 1) throw new Error("IFRAME_SELECTOR_MULTIPLE_FRAMES_MATCHED: " + iframeSelector + " count=" + matched.length);
        }
        if (urlContains) {
            const matched = frames.filter(function (f) { return String(f.href || "").indexOf(urlContains) >= 0; });
            if (matched.length === 1) return matched[0];
            if (matched.length > 1) throw new Error("FRAME_URL_MULTIPLE_MATCHED: " + urlContains + " count=" + matched.length);
            throw new Error("FRAME_URL_NOT_FOUND: " + urlContains);
        }
        if (mode === "iframe" || mode === "frame") {
            const matched = frames.filter(function (f) { return !f.is_top; });
            if (matched.length === 1) return matched[0];
            if (matched.length > 1) throw new Error("IFRAME_MULTIPLE_MATCHED: frame_id 또는 frame_url_contains를 지정하십시오. count=" + matched.length);
            throw new Error("IFRAME_NOT_FOUND");
        }
        return frames.find(function (f) { return f.is_top; }) || frames[0];
    }

    async function aiwkSelectorRun(msg) {
        const tab = await getTargetTabByMessage(msg || {});
        await ensureFrameBridge(tab);
        const req = Object.assign({}, msg.request || {}, msg.command || {}, msg || {});
        const frame = await resolveFrameForSelector(tab, req);
        if (!frame || frame.frame_id === undefined) throw new Error("FRAME_RESOLVE_FAIL");
        const request = {
            selector: req.selector || "",
            action: req.action || "find",
            value: req.value,
            deep_shadow: req.deep_shadow !== false,
            command: req.command || {}
        };
        try {
            const res = await chrome.tabs.sendMessage(tab.id, { type: "AIWK_SELECTOR_RUN_IN_FRAME", request: request }, { frameId: Number(frame.frame_id) });
            return Object.assign({ version: VERSION, tab_id: String(tab.id), frame: frame }, res || {});
        } catch (err) {
            await chrome.scripting.executeScript({ target: { tabId: tab.id, frameIds: [Number(frame.frame_id)] }, files: FRAME_BRIDGE_FILES }).catch(function () {});
            const res = await chrome.tabs.sendMessage(tab.id, { type: "AIWK_SELECTOR_RUN_IN_FRAME", request: request }, { frameId: Number(frame.frame_id) });
            return Object.assign({ version: VERSION, tab_id: String(tab.id), frame: frame }, res || {});
        }
    }

    async function aiwkIframeAgentProbe(msg) {
        const tab = await getTargetTabByMessage(msg || {});
        await ensureFrameBridge(tab);
        const req = Object.assign({}, msg.request || {}, msg.command || {}, msg || {});
        req.frame_mode = req.frame_mode || (req.frame && req.frame.mode) || "iframe";
        const frame = await resolveFrameForSelector(tab, req);
        if (!frame || frame.frame_id === undefined) throw new Error("IFRAME_AGENT_FRAME_RESOLVE_FAIL");
        if (frame.is_top && String(req.allow_top || "") !== "1") {
            throw new Error("IFRAME_AGENT_TOP_FRAME_BLOCKED: iframe 대상이 아닌 top frame이 선택되었습니다. frame_url_contains 또는 frame_id를 확인하십시오.");
        }
        try {
            const res = await chrome.tabs.sendMessage(tab.id, {
                type: "AIWK_IFRAME_AGENT_ADD_PROBE",
                request: {
                    probe_text: req.probe_text || req.value || "AIWK iframe agent probe",
                    source: req.source || "service_worker",
                    requested_at: new Date().toISOString()
                }
            }, { frameId: Number(frame.frame_id) });
            return Object.assign({ version: VERSION, tab_id: String(tab.id), frame: frame }, res || {});
        } catch (err) {
            const firstMsg = err && err.message ? err.message : String(err);
            await chrome.scripting.executeScript({ target: { tabId: tab.id, frameIds: [Number(frame.frame_id)] }, files: FRAME_BRIDGE_FILES });
            const res = await chrome.tabs.sendMessage(tab.id, {
                type: "AIWK_IFRAME_AGENT_ADD_PROBE",
                request: {
                    probe_text: req.probe_text || req.value || "AIWK iframe agent probe",
                    source: req.source || "service_worker_after_inject",
                    first_error: firstMsg,
                    requested_at: new Date().toISOString()
                }
            }, { frameId: Number(frame.frame_id) });
            return Object.assign({ version: VERSION, tab_id: String(tab.id), frame: frame }, res || {});
        }
    }


    async function aiwkIframeAgentChatSend(msg) {
        const tab = await getTargetTabByMessage(msg || {});
        await ensureFrameBridge(tab);
        const req = Object.assign({}, msg.request || {}, msg.command || {}, msg.payload || {}, msg || {});
        req.frame_mode = req.frame_mode || (req.frame && req.frame.mode) || "iframe";
        const frame = await resolveFrameForSelector(tab, req);
        if (!frame || frame.frame_id === undefined) throw new Error("IFRAME_CHAT_FRAME_RESOLVE_FAIL");
        if (frame.is_top && String(req.allow_top || "") !== "1") {
            throw new Error("IFRAME_CHAT_TOP_FRAME_BLOCKED: iframe 대상이 아닌 top frame이 선택되었습니다. frame_url_contains 또는 frame_id를 확인하십시오.");
        }
        const request = {
            selector: req.selector || req.input_selector || "#input",
            send_selector: req.send_selector || req.button_selector || "#send-button",
            value: req.value !== undefined ? req.value : (req.message !== undefined ? req.message : req.text),
            message: req.message,
            text: req.text,
            click_send: req.click_send !== false && req.no_click !== true,
            deep_shadow: req.deep_shadow !== false,
            source: req.source || "service_worker_chat_send"
        };
        if (!request.value) {
            throw new Error("IFRAME_CHAT_MESSAGE_EMPTY: value/message/text를 지정하십시오.");
        }
        try {
            const res = await chrome.tabs.sendMessage(tab.id, { type: "AIWK_IFRAME_AGENT_CHAT_SEND", request: request }, { frameId: Number(frame.frame_id) });
            return Object.assign({ version: VERSION, tab_id: String(tab.id), frame: frame }, res || {});
        } catch (err) {
            const firstMsg = err && err.message ? err.message : String(err);
            await chrome.scripting.executeScript({ target: { tabId: tab.id, frameIds: [Number(frame.frame_id)] }, files: FRAME_BRIDGE_FILES });
            const res = await chrome.tabs.sendMessage(tab.id, { type: "AIWK_IFRAME_AGENT_CHAT_SEND", request: Object.assign({}, request, { first_error: firstMsg }) }, { frameId: Number(frame.frame_id) });
            return Object.assign({ version: VERSION, tab_id: String(tab.id), frame: frame }, res || {});
        }
    }

    async function aiwkIframeAgentList(msg) {
        const listed = await aiwkSelectorListFrames(msg || {});
        const frames = (listed.frames || []).filter(function (f) { return !f.is_top; });
        return Object.assign({}, listed, { iframe_agent_count: frames.length, iframe_agents: frames });
    }



    async function aiwkModuleRegistryGet(msg) {
        const url = chrome.runtime.getURL("AIWK_COMMON/modules/module_registry.json");
        try {
            const res = await fetch(url, { cache: "no-store" });
            if (!res.ok) {
                throw new Error("MODULE_REGISTRY_HTTP_FAIL: " + res.status);
            }
            const registry = await res.json();
            if (!registry || !Array.isArray(registry.modules)) {
                throw new Error("MODULE_REGISTRY_INVALID: modules 배열이 없습니다.");
            }
            AIWK_MODULE_REGISTRY_CACHE = registry;
            return { ok: 1, version: VERSION, registry: registry, source: msg && msg.source || "", href: msg && msg.href || "", cache: 0 };
        } catch (err) {
            if (AIWK_MODULE_REGISTRY_CACHE && Array.isArray(AIWK_MODULE_REGISTRY_CACHE.modules)) {
                return { ok: 1, version: VERSION, registry: AIWK_MODULE_REGISTRY_CACHE, source: msg && msg.source || "", href: msg && msg.href || "", cache: 1, warning: err && err.message ? err.message : String(err) };
            }
            throw err;
        }
    }

    async function aiwkIframeStreamMessage(msg, sender) {
        msg = msg || {};
        const tabId = sender && sender.tab && sender.tab.id !== undefined ? sender.tab.id : Number(msg.target_tab_id || msg.tab_id || 0);
        const frameId = sender && sender.frameId !== undefined ? sender.frameId : (msg.frame_id !== undefined ? Number(msg.frame_id) : 0);
        const target = String(msg.target || msg.to || "").trim();
        const text = String(msg.text || "").trim();
        if (!tabId) throw new Error("STREAM_TAB_ID_MISSING: sender tab 또는 target_tab_id가 없습니다.");
        if (!target) throw new Error("STREAM_TARGET_EMPTY: aiwk:// 대상이 없습니다.");
        if (!text) throw new Error("STREAM_TEXT_EMPTY: 전송할 텍스트가 없습니다.");
        const packet = {
            type: "AIWK_IFRAME_STREAM_TO_WSS",
            target: target,
            text: text,
            payload: {
                text: text,
                html: String(msg.html || ""),
                collect_selector: String(msg.collect_selector || ""),
                module_id: String(msg.module_id || ""),
                frame: msg.frame || {},
                at: msg.at || new Date().toISOString(),
                ts: msg.ts || Date.now(),
                source_frame_id: String(frameId)
            }
        };
        relayPush({ dir: "stream", channel: "iframe", action: "stream_message", target: target, text: text.slice(0, 200), tab_id: tabId, frame_id: frameId, frame: msg.frame || {} });
        const topRes = await chrome.tabs.sendMessage(Number(tabId), packet, { frameId: 0 }).catch(function (err) {
            return { ok: 0, code: "TOP_CONTENT_SEND_FAIL", error: err && err.message ? err.message : String(err) };
        });
        return { ok: topRes && topRes.ok ? 1 : 0, version: VERSION, code: topRes && topRes.ok ? "STREAM_TO_WSS_OK" : "STREAM_TO_WSS_FAIL", target: target, text_len: text.length, tab_id: String(tabId), frame_id: String(frameId), top_result: topRes || null };
    }

    async function aiwkModuleInject(msg, sender) {
        msg = msg || {};
        const tabId = sender && sender.tab && sender.tab.id !== undefined ? sender.tab.id : Number(msg.target_tab_id || msg.tab_id || 0);
        const frameId = sender && sender.frameId !== undefined ? sender.frameId : (msg.frame_id !== undefined ? Number(msg.frame_id) : 0);
        const entry = String(msg.entry || "").trim();
        const moduleId = String(msg.module_id || "").trim();
        if (!tabId) throw new Error("MODULE_INJECT_TAB_ID_MISSING: sender tab 또는 target_tab_id가 없습니다.");
        if (!moduleId) throw new Error("MODULE_ID_EMPTY: module_id가 없습니다.");
        if (!entry) throw new Error("MODULE_ENTRY_EMPTY: entry가 없습니다.");
        const isModuleEntry = entry.indexOf("AIWK_COMMON/modules/") === 0;
        const isDomainEntry = entry.indexOf("AIWK_COMMON/domains/") === 0;
        if ((!isModuleEntry && !isDomainEntry) || entry.indexOf("..") >= 0 || !/\.js$/.test(entry)) {
            throw new Error("MODULE_ENTRY_BLOCKED: " + entry + " / 해결: module은 AIWK_COMMON/modules 하위, domain 전용은 AIWK_COMMON/domains/{HTTP_HOST}/{path}/index.js 하위만 허용합니다.");
        }
        const registryRes = await aiwkModuleRegistryGet({ source: "inject_validate" });
        const found = (registryRes.registry.modules || []).find(function (m) { return String(m.id) === moduleId && String(m.entry) === entry; });
        if (!found && isModuleEntry) {
            throw new Error("MODULE_NOT_IN_REGISTRY: " + moduleId + " / 해결: module_registry.json의 id/entry를 확인하십시오.");
        }
        await chrome.scripting.executeScript({ target: { tabId: Number(tabId), frameIds: [Number(frameId)] }, files: [entry] });
        relayPush({ dir: "module", channel: "domain_module", action: "inject", module_id: moduleId, entry: entry, tab_id: tabId, frame_id: frameId, href: msg.href || "" });
        return { ok: 1, version: VERSION, code: "MODULE_INJECT_OK", module_id: moduleId, entry: entry, tab_id: String(tabId), frame_id: String(frameId) };
    }


    async function aiwkDomainIndexAutoInject(msg, sender) {
        msg = msg || {};
        const tabId = sender && sender.tab && sender.tab.id !== undefined ? sender.tab.id : Number(msg.target_tab_id || msg.tab_id || 0);
        const frameId = sender && sender.frameId !== undefined ? sender.frameId : (msg.frame_id !== undefined ? Number(msg.frame_id) : 0);
        const entries = Array.isArray(msg.entries) ? msg.entries.map(String).filter(Boolean) : [];
        if (!tabId) throw new Error("DOMAIN_INDEX_TAB_ID_MISSING");
        for (const entry of entries) {
            if (entry.indexOf("AIWK_COMMON/domains/") !== 0 || entry.indexOf("..") >= 0 || !/\/index\.js$/.test(entry)) continue;
            try {
                await chrome.scripting.executeScript({ target: { tabId: Number(tabId), frameIds: [Number(frameId)] }, files: [entry] });
                relayPush({ dir: "module", channel: "domain_index", action: "auto_inject", entry: entry, tab_id: tabId, frame_id: frameId, href: msg.href || "" });
                return { ok: 1, version: VERSION, code: "DOMAIN_INDEX_AUTO_INJECT_OK", entry: entry, tab_id: String(tabId), frame_id: String(frameId) };
            } catch (err) {
                // 후보 URI 폴더가 없는 것은 정상. 다음 후보를 시도한다.
            }
        }
        return { ok: 0, version: VERSION, code: "DOMAIN_INDEX_NOT_FOUND", tried: entries.length, href: msg.href || "" };
    }

    // v023: chrome action은 default_popup을 사용한다. 아이콘 클릭 시 service_worker가 바로 창을 띄우지 않는다.
    chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
        if (!msg || !msg.type) {
            return false;
        }






        if (msg.type === "AIWK_DOMAIN_INDEX_AUTO_INJECT") {
            aiwkDomainIndexAutoInject(msg, sender).then(sendResponse).catch(function (err) {
                sendResponse({ ok: 0, version: VERSION, code: "DOMAIN_INDEX_AUTO_INJECT_FAIL", error: err && err.message ? err.message : String(err) });
            });
            return true;
        }

        if (msg.type === "AIWK_IFRAME_STREAM_MESSAGE") {
            aiwkIframeStreamMessage(msg, sender).then(sendResponse).catch(function (err) {
                sendResponse({ ok: 0, version: VERSION, code: "IFRAME_STREAM_MESSAGE_FAIL", error: err && err.message ? err.message : String(err), solution: "target aiwk://, text, top content WSS 연결 상태를 확인하십시오." });
            });
            return true;
        }

        if (msg.type === "AIWK_MODULE_REGISTRY_GET") {
            aiwkModuleRegistryGet(msg).then(sendResponse).catch(function (err) {
                sendResponse({ ok: 0, version: VERSION, code: "MODULE_REGISTRY_GET_FAIL", error: err && err.message ? err.message : String(err), solution: "AIWK_COMMON/modules/module_registry.json 파일과 JSON 문법을 확인하십시오." });
            });
            return true;
        }
        if (msg.type === "AIWK_MODULE_INJECT") {
            aiwkModuleInject(msg, sender).then(sendResponse).catch(function (err) {
                sendResponse({ ok: 0, version: VERSION, code: "MODULE_INJECT_FAIL", module_id: msg.module_id || "", entry: msg.entry || "", error: err && err.message ? err.message : String(err), solution: "module_registry.json에 등록된 AIWK_COMMON/modules 하위 JS만 실행하십시오." });
            });
            return true;
        }

        if (msg.type === "AIWK_IFRAME_AGENT_REGISTER") {
            const tabId = sender && sender.tab ? sender.tab.id : (msg.tab_id || "");
            const frameId = sender && sender.frameId !== undefined ? sender.frameId : "";
            relayPush({ dir: "frame", channel: "iframe_agent", action: "agent_register", tab_id: tabId, frame_id: frameId, frame: msg.frame || {} });
            sendResponse({ ok: 1, version: VERSION, tab_id: tabId, frame_id: frameId, bus: "service_worker_iframe_agent" });
            return true;
        }
        if (msg.type === "AIWK_IFRAME_AGENT_EVENT") {
            const tabId = sender && sender.tab ? sender.tab.id : (msg.tab_id || "");
            const frameId = sender && sender.frameId !== undefined ? sender.frameId : "";
            relayPush({ dir: "frame", channel: "iframe_agent", action: msg.action || "agent_event", tab_id: tabId, frame_id: frameId, event: msg.event || {}, frame: msg.frame || {} });
            sendResponse({ ok: 1, version: VERSION, tab_id: tabId, frame_id: frameId, bus: "service_worker_iframe_agent" });
            return true;
        }
        if (msg.type === "AIWK_IFRAME_AGENT_LIST") {
            aiwkIframeAgentList(msg).then(sendResponse).catch(function (err) {
                sendResponse({ ok: 0, version: VERSION, code: "IFRAME_AGENT_LIST_FAIL", error: err && err.message ? err.message : String(err), solution: "일반 웹 탭에서 iframe이 로드된 뒤 다시 실행하십시오." });
            });
            return true;
        }
        if (msg.type === "AIWK_IFRAME_AGENT_PROBE") {
            aiwkIframeAgentProbe(msg).then(function (res) {
                relayPush({ dir: "frame", channel: "iframe_agent", action: "probe", tab_id: res && res.tab_id || msg.target_tab_id || "", frame_id: res && res.frame ? res.frame.frame_id : "", ok: res && res.ok ? 1 : 0, code: res && res.code || "" });
                sendResponse(res);
            }).catch(function (err) {
                sendResponse({ ok: 0, version: VERSION, code: "IFRAME_AGENT_PROBE_FAIL", error: err && err.message ? err.message : String(err), solution: "AIWK_IFRAME_AGENT_LIST로 frame_id/frame_url_contains를 먼저 확인하십시오." });
            });
            return true;
        }
        if (msg.type === "AIWK_IFRAME_AGENT_CHAT_SEND") {
            aiwkIframeAgentChatSend(msg).then(function (res) {
                relayPush({ dir: "frame", channel: "iframe_agent", action: "chat_send", tab_id: res && res.tab_id || msg.target_tab_id || "", frame_id: res && res.frame ? res.frame.frame_id : "", ok: res && res.ok ? 1 : 0, code: res && res.code || "" });
                sendResponse(res);
            }).catch(function (err) {
                sendResponse({ ok: 0, version: VERSION, code: "IFRAME_AGENT_CHAT_SEND_FAIL", error: err && err.message ? err.message : String(err), solution: "AIWK_IFRAME_AGENT_LIST로 frame_id/frame_url_contains를 확인하고, selector/value/send_selector를 지정하십시오." });
            });
            return true;
        }
        if (msg.type === "AIWK_SELECTOR_FRAME_REGISTER") {
            const tabId = sender && sender.tab ? sender.tab.id : (msg.tab_id || "");
            const frameId = sender && sender.frameId !== undefined ? sender.frameId : "";
            relayPush({ dir: "frame", channel: "selector", action: "frame_register", tab_id: tabId, frame_id: frameId, frame: msg.frame || {} });
            sendResponse({ ok: 1, version: VERSION, tab_id: tabId, frame_id: frameId });
            return true;
        }
        if (msg.type === "AIWK_SELECTOR_LIST_FRAMES") {
            aiwkSelectorListFrames(msg).then(sendResponse).catch(function (err) {
                sendResponse({ ok: 0, version: VERSION, code: "SELECTOR_LIST_FRAMES_FAIL", error: err && err.message ? err.message : String(err) });
            });
            return true;
        }
        if (msg.type === "AIWK_SELECTOR_RUN") {
            aiwkSelectorRun(msg).then(function (res) {
                relayPush({ dir: "selector", channel: "selector", action: msg.action || "find", tab_id: res && res.tab_id || msg.target_tab_id || "", frame_id: res && res.frame ? res.frame.frame_id : "", ok: res && res.ok ? 1 : 0, code: res && res.code || "" });
                sendResponse(res);
            }).catch(function (err) {
                sendResponse({ ok: 0, version: VERSION, code: "SELECTOR_RUN_FAIL", error: err && err.message ? err.message : String(err) });
            });
            return true;
        }

        if (msg.type === "AIWK_RELAY_EVENT") {
            const tabId = sender && sender.tab ? sender.tab.id : (msg.tab_id || "");
            const row = Object.assign({}, msg.event || {}, { tab_id: tabId, sender_url: sender && sender.tab ? sender.tab.url : "" });
            if (tabId) {
                const key = relayTabKey(tabId);
                AIWK_RELAY_TAB_STATE[key] = Object.assign({}, AIWK_RELAY_TAB_STATE[key] || {}, row.tab_state || {}, { tab_id: tabId, last_seen: Date.now(), title: sender && sender.tab ? sender.tab.title : "", url: sender && sender.tab ? sender.tab.url : "" });
            }
            sendResponse({ ok: 1, version: VERSION, row: relayPush(row) });
            return true;
        }
        if (msg.type === "AIWK_RELAY_GET_STATE") {
            relaySnapshot().then(sendResponse).catch(function (err) { sendResponse({ ok: 0, version: VERSION, error: err && err.message ? err.message : String(err) }); });
            return true;
        }
        if (msg.type === "AIWK_RELAY_ACTIVATE_TAB") {
            relayActivateTab(msg.tab_id || msg.tabId || msg.target_tab_id).then(sendResponse).catch(function (err) { sendResponse({ ok: 0, version: VERSION, code: "TAB_ACTIVATE_FAIL", error: err && err.message ? err.message : String(err) }); });
            return true;
        }
        if (msg.type === "AIWK_RELAY_STOP_ALL") {
            relayBroadcast({ type: "AIWK_STOP_ALL_LOOPS", reason: msg.reason || "relay-window" }).then(sendResponse).catch(function (err) { sendResponse({ ok: 0, version: VERSION, error: err && err.message ? err.message : String(err) }); });
            return true;
        }
        if (msg.type === "AIWK_RELAY_RESET_COUNTERS") {
            relayBroadcast({ type: "AIWK_RESET_COUNTERS_FROM_RELAY", reason: msg.reason || "relay-window" }).then(sendResponse).catch(function (err) { sendResponse({ ok: 0, version: VERSION, error: err && err.message ? err.message : String(err) }); });
            return true;
        }
        if (msg.type === "AIWK_OPEN_RELAY_PANEL" || msg.type === "AIWK_OPEN_RELAY_WINDOW" || msg.type === "AIWK_RELAY_OPEN_POPUP") {
            if (msg.target_tab_id || msg.tab_id || msg.tabId) AIWK_RELAY_ACTIVE_BROWSER_TAB_ID = String(msg.target_tab_id || msg.tab_id || msg.tabId);
            relayOpenPopup().then(sendResponse).catch(function (err) { sendResponse({ ok: 0, version: VERSION, error: err && err.message ? err.message : String(err) }); });
            return true;
        }
        if (msg.type === "AIWK_RELAY_OPEN_SIDEPANEL") {
            relayClosePopup().then(function () { return relayOpenSidePanel(); }).then(sendResponse).catch(function (err) { sendResponse({ ok: 0, version: VERSION, error: err && err.message ? err.message : String(err) }); });
            return true;
        }
        if (msg.type === "AIWK_RELAY_DETACH_POPUP") {
            relayOpenPopup().then(sendResponse).catch(function (err) { sendResponse({ ok: 0, version: VERSION, error: err && err.message ? err.message : String(err) }); });
            return true;
        }

        if (msg.type === "AIWK_POPUP_TO_ACTIVE_TAB") {
            sendActiveTabWithEnsure(msg.message || {}, msg.target_tab_id || (msg.message && msg.message.target_tab_id) || "").then(function (res) {
                sendResponse(Object.assign({ ok: 1, version: VERSION }, res || {}));
            }).catch(function (err) {
                sendResponse({ ok: 0, version: VERSION, code: "ACTIVE_TAB_MESSAGE_FAIL", error: err && err.message ? err.message : String(err) });
            });
            return true;
        }

        if (msg.type === "AIWK_FIND_WEB1_AND_DELIVER_REPORT") {
            findWeb1TabAndDeliver(msg.report || {}).then(function (res) {
                sendResponse(Object.assign({ version: VERSION }, res || {}));
            }).catch(function (err) {
                sendResponse({ ok: 0, version: VERSION, code: "WEB1_DELIVER_ROUTE_FAIL", error: err && err.message ? err.message : String(err) });
            });
            return true;
        }

        if (msg.type === "AIWK_FIND_CHATGPT_AND_INSERT_REPORT") {
            findChatGptTabAndInsert(msg.text || "").then(function (res) {
                sendResponse(Object.assign({ version: VERSION }, res || {}));
            }).catch(function (err) {
                sendResponse({ ok: 0, version: VERSION, code: "CHATGPT_INSERT_ROUTE_FAIL", error: err && err.message ? err.message : String(err) });
            });
            return true;
        }
        if (msg.type === "AIWK_AUTH_FETCH_REMOTE_TOKEN_BUS") {
            readTokenByTab().then(function (res) {
                sendResponse(res);
            }).catch(function (err) {
                replyError(sendResponse, "REMOTE_TOKEN_TABBUS_READ_FAIL", err && err.message ? err.message : String(err));
            });
            return true;
        }
        if (msg.type === "AIWK_AUTH_VERIFY_REMOTE_TOKEN") {
            verifyRemoteToken(msg.token).then(function (res) {
                sendResponse(Object.assign({ ok: 1, version: VERSION, code: "TOKEN_VERIFIED" }, res));
            }).catch(function (err) {
                replyError(sendResponse, "TOKEN_VERIFY_FAIL", err && err.message ? err.message : String(err), { verified: 0 });
            });
            return true;
        }
        if (msg.type === "AIWK_GET_PROFILE_INSTANCE_ID") {
            getProfileInstanceId().then(function (id) {
                sendResponse({ ok: 1, version: VERSION, profile_instance_id: id, profileInstanceId: id });
            }).catch(function (err) {
                sendResponse({ ok: 0, version: VERSION, code: "PROFILE_INSTANCE_ID_FAIL", error: err && err.message ? err.message : String(err) });
            });
            return true;
        }
        if (msg.type === "AIWK_GET_TAB_ID") {
            const tabId = sender && sender.tab ? sender.tab.id : "";
            sendResponse({ ok: tabId ? 1 : 0, version: VERSION, tabId: tabId, tab_id: tabId });
            return true;
        }
        if (msg.type === "AIWK_GET_CURRENT_TAB_CONFIG") {
            const tabId = sender && sender.tab ? sender.tab.id : "";
            if (!tabId) {
                sendResponse({ ok: 0, version: VERSION, code: "TAB_ID_MISSING", tab_id: "" });
                return true;
            }
            const routeKey = String(msg.route_key || msg.url_key || "").trim();
            const key = routeConfigKey(routeKey);
            chrome.storage.local.get([key]).then(async function (obj) {
                let cfg = obj && obj[key] ? obj[key] : null;
                let code = "TAB_CONFIG_FOUND";
	                if (!cfg) {
	                    const profileInstanceId = await getProfileInstanceId();
	                    cfg = {
                        tab_id: String(tabId),
                        route_key: routeKey,
                        url_key: routeKey,
                        profile_instance_id: profileInstanceId,
                        profileInstanceId: profileInstanceId,
                        title: sender && sender.tab ? (sender.tab.title || "") : "",
                        url: sender && sender.tab ? (sender.tab.url || "") : "",
                        status: "NEW",
                        aiwk_status: "NEW",
                        assigned: false,
                        ended: false,
                        created_at: new Date().toISOString(),
                        updated_at: new Date().toISOString(),
	                        source: "service_worker.first_run"
	                    };
	                    code = "TAB_CONFIG_NEW";
	                }
                cfg = mergeRouteTabState(cfg, sender && sender.tab ? sender.tab : { id: tabId }, { enabled: (cfg.tabs && cfg.tabs[String(tabId)] && cfg.tabs[String(tabId)].enabled === false) ? false : true, source: cfg ? "service_worker.get_current" : "service_worker.first_run" });
                await chrome.storage.local.set({ [key]: cfg });
                sendResponse({ ok: 1, version: VERSION, tabId: tabId, tab_id: String(tabId), route_key: cfg.route_key || routeKey, url_key: cfg.url_key || routeKey, tab_state: cfg.tabs && cfg.tabs[String(tabId)] || {}, profile_instance_id: cfg.profile_instance_id || cfg.profileInstanceId || "", config: cfg, code: code });
            }).catch(function (err) {
                sendResponse({ ok: 0, version: VERSION, tabId: tabId, tab_id: String(tabId), code: "TAB_CONFIG_READ_FAIL", error: err && err.message ? err.message : String(err) });
            });
            return true;
        }
        if (msg.type === "AIWK_EXT4_OPEN_MANUAL_TEST_FROM_POPUP") {
            openAuthOnActiveTab(msg.config || {}).then(function () {
                return chrome.tabs.query({ active: true, currentWindow: true });
            }).then(function (tabs) {
                const tab = tabs && tabs[0] ? tabs[0] : null;
                if (!tab || !tab.id) { throw new Error("ACTIVE_TAB_NOT_FOUND"); }
                return sendMessageToTab(tab.id, { type: "AIWK_EXT4_OPEN_MANUAL_TEST", config: msg.config || {} });
            }).then(function (res) {
                sendResponse(Object.assign({ ok: 1, version: VERSION }, res || {}));
            }).catch(function (err) {
                sendResponse({ ok: 0, version: VERSION, code: "OPEN_MANUAL_TEST_FAIL", error: err && err.message ? err.message : String(err) });
            });
            return true;
        }
        if (msg.type === "AIWK_EXT4_CONTROL_FROM_POPUP") {
            controlActiveTab(msg.action || "").then(function (res) {
                sendResponse(Object.assign({ ok: 1, version: VERSION }, res || {}));
            }).catch(function (err) {
                sendResponse({ ok: 0, version: VERSION, code: "WIN_CONTROL_FAIL", action: msg.action || "", error: err && err.message ? err.message : String(err) });
            });
            return true;
        }
        if (msg.type === "AIWK_EXT4_OPEN_MAIN_FROM_POPUP") {
            openAuthOnActiveTab(msg.config || {}).then(function (res) {
                sendResponse(Object.assign({ ok: 1, version: VERSION }, res || {}));
            }).catch(function (err) {
                sendResponse({ ok: 0, version: VERSION, code: "OPEN_MAIN_FAIL", error: err && err.message ? err.message : String(err) });
            });
            return true;
        }
        if (msg.type === "AIWK_TAB_BUS_REGISTER") {
            const tabId = sender && sender.tab ? sender.tab.id : "";
            const profile = msg.profile || {};
            const endpoint = String(msg.endpoint || "tabbus").replace(/^#/, "") || "tabbus";
            const key = aiwkTabRoomKey(profile) + "." + String(profile.clientId || "C1") + "#" + endpoint;
            AIWK_TAB_BUS_REGISTRY[key] = {
                key: key,
                room_key: aiwkTabRoomKey(profile),
                tab_id: tabId,
                client_id: String(profile.clientId || "C1"),
                endpoint: endpoint,
                address: msg.address || key,
                page: msg.page || {},
                last_seen: Date.now()
            };
            sendResponse({ ok: 1, version: VERSION, tabId: tabId, tab_id: tabId, registered: AIWK_TAB_BUS_REGISTRY[key], clients: aiwkTabRegistryList(profile) });
            return true;
        }
        if (msg.type === "AIWK_TAB_BUS_UNREGISTER") {
            const tabId = sender && sender.tab ? sender.tab.id : "";
            const profile = msg.profile || {};
            const endpoint = String(msg.endpoint || "tabbus").replace(/^#/, "") || "tabbus";
            const key = aiwkTabRoomKey(profile) + "." + String(profile.clientId || "C1") + "#" + endpoint;
            if (AIWK_TAB_BUS_REGISTRY[key]) delete AIWK_TAB_BUS_REGISTRY[key];
            Object.keys(AIWK_TAB_BUS_REGISTRY).forEach(function (k) {
                const row = AIWK_TAB_BUS_REGISTRY[k] || {};
                if (String(row.tab_id || "") === String(tabId) && String(row.client_id || "") === String(profile.clientId || "C1") && String(row.endpoint || "") === endpoint) {
                    delete AIWK_TAB_BUS_REGISTRY[k];
                }
            });
            sendResponse({ ok: 1, version: VERSION, tabId: tabId, tab_id: tabId, unregistered: key, clients: aiwkTabRegistryList(profile) });
            return true;
        }
        if (msg.type === "AIWK_TAB_BUS_LIST") {
            sendResponse({ ok: 1, version: VERSION, clients: aiwkTabRegistryList(msg.profile || {}) });
            return true;
        }
        if (msg.type === "AIWK_TAB_BUS_SEND") {
            const profile = msg.profile || {};
            const packet = msg.packet || {};
            const parsed = aiwkParseTo(packet.to || "*");
            const list = aiwkTabRegistryList(profile).filter(row => aiwkClientPatternMatch(row.client_id, parsed.client) && (!parsed.endpoint || row.endpoint === parsed.endpoint));
            Promise.all(list.map(row => chrome.tabs.sendMessage(row.tab_id, { type: "AIWK_TAB_BUS_DELIVER", packet: Object.assign({}, packet, { _target: row }) }).then(() => ({ ok:1, tab_id: row.tab_id, client_id: row.client_id, endpoint: row.endpoint })).catch(err => ({ ok:0, tab_id: row.tab_id, client_id: row.client_id, endpoint: row.endpoint, error: err && err.message ? err.message : String(err) })))).then(results => {
                sendResponse({ ok: 1, version: VERSION, count: results.length, results: results, matched: list });
            });
            return true;
        }
        return false;
    });
}());
