// /AIWK_EXT4/aiwk_uri_runner.js
// AIWK EXT4 v051 · aiwk:// URI click runner + chat/ping/route responder + 양방향 반복 Ping/Pong
(function () {
    "use strict";

    const AIWK = window.AIWK || (window.AIWK = {});
    const pending = {};
    const LOOP_KEY = "AIWK_URI_PING_LOOP_ENABLED_V1";
    const LOOP_MS_KEY = "AIWK_URI_PING_LOOP_MS_V1";
    let zIndex = 2147482000;

    function s(v) { return String(v === undefined || v === null ? "" : v); }
    function json(v) { try { return JSON.stringify(v || {}, null, 2); } catch (err) { return String(v); } }
    function esc(v) { return s(v).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"); }
    function loopEnabled() { try { return localStorage.getItem(LOOP_KEY) === "1"; } catch (err) { return false; } }
    function loopMs() { try { return Math.max(500, parseInt(localStorage.getItem(LOOP_MS_KEY) || "3000", 10) || 3000); } catch (err) { return 3000; } }

    function injectCss() {
        if (document.getElementById("aiwk-uri-runner-css")) return;
        const st = document.createElement("style");
        st.id = "aiwk-uri-runner-css";
        st.textContent = [
            '.aiwk-uri-runner{position:fixed;right:24px;bottom:24px;width:560px;max-width:calc(100vw - 32px);max-height:calc(100vh - 32px);z-index:2147482000;background:rgba(17,24,39,.97);color:#e5e7eb;border:1px solid rgba(255,255,255,.18);border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,.45);font:13px/1.45 Arial,"Noto Sans KR",sans-serif;overflow:hidden;}',
            '.aiwk-uri-head{display:flex;align-items:center;gap:8px;padding:10px 12px;background:rgba(31,41,55,.98);cursor:move;user-select:text;}',
            '.aiwk-uri-head b{flex:1;font-size:14px;}.aiwk-uri-close{border:0;background:#374151;color:#f9fafb;border-radius:999px;padding:3px 8px;cursor:pointer;}',
            '.aiwk-uri-body{padding:12px;max-height:calc(100vh - 100px);overflow:auto;}',
            '.aiwk-uri-row{margin:8px 0;}.aiwk-uri-row code{display:block;white-space:pre-wrap;word-break:break-all;background:#020617;border:1px solid rgba(255,255,255,.12);border-radius:8px;padding:7px;margin-top:4px;color:#d1fae5;}',
            '.aiwk-uri-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:8px 0;}.aiwk-uri-grid label{display:flex;flex-direction:column;gap:3px;color:#9ca3af;font-size:12px;}',
            '.aiwk-uri-grid input,.aiwk-uri-row input,.aiwk-uri-loop input{background:#020617;color:#f9fafb;border:1px solid rgba(255,255,255,.16);border-radius:7px;padding:6px;min-height:1.4em;}',
            '.aiwk-uri-title-note{padding:7px 9px;border-radius:8px;background:rgba(37,99,235,.16);border:1px solid rgba(96,165,250,.24);margin-bottom:8px;}',
            '.aiwk-uri-source{font-size:12px;color:#93c5fd;background:rgba(30,64,175,.18);border:1px solid rgba(147,197,253,.25);border-radius:8px;padding:6px 8px;margin-bottom:8px;user-select:text;}',
            '.aiwk-uri-row textarea{box-sizing:border-box;width:100%;min-height:58px;resize:vertical;background:#020617;color:#f9fafb;border:1px solid rgba(255,255,255,.16);border-radius:8px;padding:8px;font:12px/1.45 Consolas,monospace;}',
            '.aiwk-uri-loop{display:flex;gap:8px;align-items:center;flex-wrap:wrap;border:1px solid rgba(251,191,36,.22);background:rgba(251,191,36,.08);border-radius:10px;padding:8px;margin:8px 0;}.aiwk-uri-loop label{display:flex;align-items:center;gap:5px;}.aiwk-uri-loop-note{font-size:12px;color:#fde68a;width:100%;}',
            '.aiwk-chat-pop{position:fixed;right:24px;bottom:24px;z-index:2147481999;max-width:450px;background:rgba(3,7,18,.96);border:1px solid rgba(147,197,253,.35);border-radius:12px;color:#e5e7eb;padding:10px;box-shadow:0 15px 40px rgba(0,0,0,.4);font:13px/1.45 Arial,"Noto Sans KR",sans-serif;}',
            '.aiwk-uri-actions{display:flex;gap:8px;align-items:center;margin:10px 0;}.aiwk-uri-actions button{border:1px solid rgba(255,255,255,.18);border-radius:999px;background:#2563eb;color:#fff;padding:7px 12px;cursor:pointer;}.aiwk-uri-actions button:hover{background:#1d4ed8;}',
            '.aiwk-uri-result,.aiwk-uri-row pre{white-space:pre-wrap;word-break:break-word;background:#020617;color:#e5e7eb;border:1px solid rgba(255,255,255,.12);border-radius:9px;padding:8px;min-height:56px;font:12px/1.4 Consolas,monospace;}',
            '.aiwk-uri-error{color:#fecaca;}.aiwk-uri-ok{color:#bbf7d0;}'
        ].join("\n");
        (document.head || document.documentElement).appendChild(st);
    }

    function makeDraggable(win, head) {
        let sx = 0, sy = 0, ox = 0, oy = 0, moving = false;
        head.addEventListener("mousedown", function (e) {
            if (e.button !== 0 || e.target.closest("button")) return;
            moving = true; sx = e.clientX; sy = e.clientY;
            const r = win.getBoundingClientRect(); ox = r.left; oy = r.top;
            win.style.left = ox + "px"; win.style.top = oy + "px"; win.style.right = "auto"; win.style.bottom = "auto";
            e.preventDefault();
        });
        document.addEventListener("mousemove", function (e) {
            if (!moving) return;
            win.style.left = Math.max(0, ox + e.clientX - sx) + "px";
            win.style.top = Math.max(0, oy + e.clientY - sy) + "px";
        });
        document.addEventListener("mouseup", function () { moving = false; });
    }

    function applyUriAuth(parsed) {
        if (!parsed || !parsed.token) return;
        try {
            const p = AIWK.store.get("profile", {}) || {};
            p.authToken = parsed.token;
            p.token = parsed.token;
            p.group = parsed.group || p.group || "home";
            p.project = parsed.project || p.project || "IG_CRM";
            if (parsed.room_id) p.roomId = parsed.room_id;
            AIWK.store.set("profile", p);
        } catch (err) {}
    }

    async function ensureReady(parsed) {
        applyUriAuth(parsed);
        if (AIWK.ext4 && typeof AIWK.ext4.boot === "function") return await AIWK.ext4.boot();
        return { ok: 1, boot: "not_required" };
    }

    function showRunner(parsed) {
        injectCss();
        const ui = (window.AIWK_ACTION_UI && (window.AIWK_ACTION_UI[parsed.action] || window.AIWK_ACTION_UI._default)) || { title: "AIWK URI 실행창", render: function () { return '<pre data-aiwk-result>AIWK_ACTION_UI 없음</pre>'; } };
        const win = document.createElement("div");
        win.className = "aiwk-uri-runner";
        win.style.zIndex = String(++zIndex);
        const head = document.createElement("div");
        head.className = "aiwk-uri-head";
        const title = document.createElement("b");
        title.textContent = "EXT4 · " + (ui.title || parsed.action || "AIWK URI 실행창");
        const close = document.createElement("button");
        close.type = "button"; close.className = "aiwk-uri-close"; close.textContent = "닫기";
        head.appendChild(title); head.appendChild(close);
        const body = document.createElement("div");
        body.className = "aiwk-uri-body";
        const ctx = { parsed: parsed, win: win, body: body, ui: ui, resultBox: null, packet: null, sent_at: 0, loopTimer: null, loopActive: false };
        body.innerHTML = ui.render ? ui.render(ctx) : "";
        ctx.resultBox = body.querySelector("[data-aiwk-result]");
        win.appendChild(head); win.appendChild(body);
        document.documentElement.appendChild(win);
        makeDraggable(win, head);
        function closeWin() { if (ctx.loopTimer) clearTimeout(ctx.loopTimer); ctx.loopActive = false; win.remove(); }
        close.onclick = closeWin;
        const cancel = body.querySelector("[data-aiwk-cancel]");
        if (cancel) cancel.onclick = closeWin;
        const run = body.querySelector("[data-aiwk-run]");
        if (run) run.onclick = function () { execute(ctx); };
        return ctx;
    }

    function setResult(ctx, text, cls) {
        if (!ctx || !ctx.resultBox) return;
        ctx.resultBox.classList.remove("aiwk-uri-error", "aiwk-uri-ok");
        if (cls) ctx.resultBox.classList.add(cls);
        ctx.resultBox.textContent = text;
    }

    function sendPacket(packet) {
        if (AIWK.comm && typeof AIWK.comm.wssSendPacket === "function") return AIWK.comm.wssSendPacket(packet);
        return { ok: 0, code: "AIWK_COMM_WSS_SEND_PACKET_MISSING", error: "AIWK.comm.wssSendPacket 없음" };
    }

    async function execute(ctx) {
        await ensureReady(ctx.parsed);
        if (!AIWK.protocol || !AIWK.comm) { setResult(ctx, "AIWK.protocol 또는 AIWK.comm 없음", "aiwk-uri-error"); return; }
        if (ctx.loopTimer) { clearTimeout(ctx.loopTimer); ctx.loopTimer = null; }
        if (ctx.ui && typeof ctx.ui.beforeSend === "function") ctx.parsed = ctx.ui.beforeSend(ctx, Object.assign({}, ctx.parsed || {})) || ctx.parsed;
        const packet = AIWK.protocol.buildEnvelope(ctx.parsed, { from_endpoint: "ext4" });
        ctx.packet = packet;
        ctx.sent_at = Date.now();
        ctx.loopActive = !!(packet.action === "aiwk_ping" && packet.payload && packet.payload.loop_request);
        pending[packet.message_id] = ctx;
        setResult(ctx, "전송 중\n" + json(redactPacket(packet)), "");
        const res = sendPacket(packet);
        if (!res || !res.ok) { delete pending[packet.message_id]; ctx.loopActive = false; setResult(ctx, "전송 실패\n" + json(res || {}), "aiwk-uri-error"); return; }
        setResult(ctx, "전송 완료 / result 대기\nmessage_id=" + packet.message_id + "\n" + json(redactPacket(res.packet || packet)), "aiwk-uri-ok");
    }

    function redactPacket(packet) {
        const p = Object.assign({}, packet || {});
        if (p.uri && p.token && AIWK.protocol && AIWK.protocol.maskToken) p.uri = String(p.uri).replace(p.token, AIWK.protocol.maskToken(p.token));
        if (p.token) p.token = AIWK.protocol && AIWK.protocol.maskToken ? AIWK.protocol.maskToken(p.token) : "***";
        return p;
    }

    function currentClient() {
        const p = AIWK.comm && AIWK.comm.getProfile ? AIWK.comm.getProfile() : {};
        return { client_id: p.clientId || "C1", group: p.group || "home", project: p.project || "IG_CRM", room_id: p.roomId || "" };
    }

    function msgPayload(msg) { return (msg && (msg.payload || msg.args || msg.data)) || {}; }

    function replyToCommand(msg, action, payload) {
        const me = currentClient();
        const packet = {
            v: "AIWK_MSG_1",
            type: "result",
            action: action,
            uri: msg.uri || "",
            chat_id: msg.chat_id || ((msg.group || me.group) + "." + (msg.project || me.project)),
            group: msg.group || me.group,
            project: msg.project || me.project,
            room_id: msg.room_id || me.room_id,
            message_id: AIWK.protocol.uid("r"),
            msg_id: "",
            reply_to_message_id: msg.message_id || msg.msg_id || "",
            reply_to: msg.message_id || msg.msg_id || "",
            from: AIWK.protocol.endpointAddress(msg.group || me.group, msg.project || me.project, me.client_id, "ext4"),
            from_client: me.client_id,
            from_endpoint: "ext4",
            to: msg.from || (msg.from_client ? AIWK.protocol.endpointAddress(msg.group || me.group, msg.project || me.project, msg.from_client, msg.from_endpoint || "ext4") : "HOST1"),
            target: msg.from_client ? AIWK.protocol.cleanAddress(msg.group || me.group, msg.project || me.project, msg.from_client) : "HOST1",
            to_client: msg.from_client || "",
            to_endpoint: msg.from_endpoint || "",
            client_ts: Date.now(),
            sent_at_ms: Date.now(),
            payload: payload || {},
            args: payload || {}
        };
        packet.msg_id = packet.message_id;
        return sendPacket(packet);
    }

    function showIncomingChat(msg) {
        injectCss();
        const old = document.getElementById("aiwk-chat-pop");
        if (old) old.remove();
        const pop = document.createElement("div");
        pop.id = "aiwk-chat-pop";
        pop.className = "aiwk-chat-pop";
        const p = msgPayload(msg);
        const text = msg.text || msg.message || p.text || "";
        const fromAddr = msg.from || msg.from_client || "";
        pop.innerHTML = [
            '<div style="display:flex;gap:8px;align-items:center;margin-bottom:6px;"><b style="flex:1;">AIWK Chat 수신</b><button type="button" data-close="1" style="border:0;border-radius:999px;background:#374151;color:#fff;padding:3px 8px;cursor:pointer;">닫기</button></div>',
            '<div style="color:#9ca3af;font-size:12px;margin-bottom:6px;">from: ' + esc(fromAddr) + ' → to: ' + esc(msg.to_client || msg.to || "") + '</div>',
            '<pre style="white-space:pre-wrap;word-break:break-word;background:#020617;border:1px solid rgba(255,255,255,.12);border-radius:8px;padding:8px;max-height:220px;overflow:auto;">' + esc(text || json(p)) + '</pre>',
            '<div style="margin-top:6px;"><input data-reply-to value="' + esc(fromAddr) + '" style="box-sizing:border-box;width:100%;background:#020617;color:#fff;border:1px solid rgba(255,255,255,.16);border-radius:7px;padding:6px;margin-bottom:5px;"><textarea data-reply-text placeholder="답장 입력" style="box-sizing:border-box;width:100%;min-height:48px;background:#020617;color:#fff;border:1px solid rgba(255,255,255,.16);border-radius:7px;padding:6px;"></textarea><button type="button" data-reply-send style="margin-top:5px;border:0;border-radius:999px;background:#2563eb;color:#fff;padding:5px 10px;cursor:pointer;">답장</button></div>'
        ].join("");
        document.documentElement.appendChild(pop);
        const btn = pop.querySelector('[data-close]');
        if (btn) btn.onclick = function () { pop.remove(); };
        const send = pop.querySelector('[data-reply-send]');
        if (send) send.onclick = function () {
            const to = pop.querySelector('[data-reply-to]').value.trim();
            const txt = pop.querySelector('[data-reply-text]').value;
            const me = currentClient();
            let toClient = msg.from_client || to;
            let toEndpoint = msg.from_endpoint || "ext4";
            if (AIWK.protocol && AIWK.protocol.parseTargetAddress) {
                const parsedTo = AIWK.protocol.parseTargetAddress(to, { group: msg.group || me.group, project: msg.project || me.project, endpoint: toEndpoint });
                if (parsedTo.client_id) toClient = parsedTo.client_id;
                if (parsedTo.endpoint) toEndpoint = parsedTo.endpoint;
            }
            const parsed = { group: msg.group || me.group, project: msg.project || me.project, room_id: msg.room_id || me.room_id, to: toClient, client_id: toClient, endpoint: toEndpoint, to_endpoint: toEndpoint, type: "chat", action: "aiwk_chat", payload: { text: txt } };
            const packet = AIWK.protocol.buildEnvelope(parsed, { from_endpoint: "ext4" });
            sendPacket(packet);
        };
        setTimeout(function () { if (pop && pop.parentNode) pop.remove(); }, 30000);
    }

    function handleIncomingCommand(msg) {
        if (!AIWK.protocol || !AIWK.protocol.isForMe(msg, "ext4")) return;
        const p = msgPayload(msg);
        if (msg.action === "aiwk_ping") {
            replyToCommand(msg, "aiwk_pong", { ok: 1, kind: "pong", received_at_ms: Date.now(), from_client: currentClient().client_id, echo_message_id: msg.message_id || msg.msg_id || "", loop_enabled: loopEnabled(), loop_ms: loopMs(), loop_request: !!p.loop_request });
            return;
        }
        if (msg.action === "aiwk_chat" || msg.type === "chat") {
            showIncomingChat(msg);
            replyToCommand(msg, "aiwk_chat_ack", { ok: 1, kind: "chat_ack", received_at_ms: Date.now(), from_client: currentClient().client_id, text: msg.text || msg.message || p.text || "", echo_message_id: msg.message_id || msg.msg_id || "" });
            return;
        }
        if (msg.action === "aiwk_route") {
            replyToCommand(msg, "aiwk_route_result", {
                ok: 1,
                kind: "route_result",
                received_at_ms: Date.now(),
                from_client: currentClient().client_id,
                received_to: msg.to || "",
                received_target: msg.target || "",
                received_route: msg.route || {},
                resolved_here: currentClient(),
                note: "EXT4 대상 client가 직접 회신한 route 결과입니다. Hub 자동 응답이 아닙니다."
            });
        }
    }

    function handleResult(msg) {
        const replyId = msg.reply_to_message_id || msg.reply_to || "";
        if (!replyId || !pending[replyId]) return;
        const ctx = pending[replyId];
        const latency = Date.now() - (ctx.sent_at || Date.now());
        delete pending[replyId];
        if (ctx.ui && typeof ctx.ui.onResult === "function") ctx.ui.onResult(ctx, msg, { latency_ms: latency });
        else setResult(ctx, "결과 수신 " + latency + "ms\n" + json(msg), "aiwk-uri-ok");

        const payload = msgPayload(msg);
        if (ctx.packet && ctx.packet.action === "aiwk_ping" && ctx.loopActive) {
            const localOk = loopEnabled();
            const remoteOk = !!payload.loop_enabled;
            const ms = Math.max(500, parseInt(payload.loop_ms || ctx.packet.payload.loop_ms || loopMs(), 10) || 3000);
            if (localOk && remoteOk && ctx.win && ctx.win.parentNode) {
                ctx.loopTimer = setTimeout(function () { execute(ctx); }, ms);
                if (ctx.resultBox) ctx.resultBox.textContent += "\n\n반복 Ping/Pong 예약: " + ms + "ms 후 재전송";
            } else if (ctx.resultBox) {
                ctx.resultBox.textContent += "\n\n반복 중지: local=" + (localOk ? "ON" : "OFF") + " / remote=" + (remoteOk ? "ON" : "OFF");
            }
        }
    }

    function onWssMessage(msg) {
        if (!msg || typeof msg !== "object") return;
        if (msg.type === "command" || msg.type === "chat") handleIncomingCommand(msg);
        if (msg.type === "result" || /_result$|_pong$|_ack$/i.test(String(msg.action || ""))) handleResult(msg);
    }

    function onClick(e) {
        const a = e.target && e.target.closest ? e.target.closest('a[href^="aiwk://"],[data-aiwk-uri^="aiwk://"]') : null;
        if (!a) return;
        const uri = a.getAttribute("href") || a.getAttribute("data-aiwk-uri") || "";
        const dataPayload = a.getAttribute("data-payload") || "";
        const parsed = AIWK.protocol && AIWK.protocol.parseUri ? AIWK.protocol.parseUri(uri, dataPayload) : { ok: 0, error: "AIWK.protocol 없음", uri: uri };
        e.preventDefault(); e.stopPropagation();
        if (!parsed.ok) {
            injectCss();
            const ctx = showRunner({ uri: uri, action: "AIWK_URI_ERROR", payload: { error: parsed.error || parsed.code } });
            setResult(ctx, json(parsed), "aiwk-uri-error");
            return;
        }
        showRunner(parsed);
    }

    function init() {
        if (document.__aiwkUriRunnerBound) return;
        document.__aiwkUriRunnerBound = true;
        document.addEventListener("click", onClick, true);
        if (AIWK.events && typeof AIWK.events.on === "function") AIWK.events.on("comm:wss:message", onWssMessage);
    }

    AIWK.uriRunner = { init: init, showRunner: showRunner, execute: execute, pending: pending, loopEnabled: loopEnabled, loopMs: loopMs };
    if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init, { once: true }); else init();
}());
