// /aiwk/AIWK_RELAY/aiwk_relay_map.js
// AIWK Relay map module v109.2-relay-map-stable
(function () {
  "use strict";

  const VERSION = "v109.2-relay-map-stable";

  function esc(s) {
    return String(s === undefined || s === null ? "" : s).replace(/[&<>"]/g, function (c) {
      return {"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;"}[c];
    });
  }

  function val(it, keys, def) {
    it = it || {};
    for (const k of keys) {
      if (it[k] !== undefined && it[k] !== null && String(it[k]) !== "") return it[k];
    }
    return def;
  }

  function tabIdOfItem(it) {
    it = it || {};
    return String(it.tab_id || (it.tab && it.tab.tab_id) || "").trim();
  }
  function routeKeyOfItem(it) {
    it = it || {};
    return String(it.route_key || it.url_key || (it.tab && (it.tab.route_key || it.tab.url_key)) || "").trim();
  }

  function makeId(prefix, parts) {
    return prefix + ":" + parts.map(function (x) { return String(x || "-").replace(/\s+/g, "_"); }).join(":");
  }

  function child(parent, node) {
    parent.children = parent.children || [];
    parent.children.push(node);
    return node;
  }

  const OPEN_STATE_KEY = "AIWK_RELAY_MAP_OPEN_STATE_V109";

  function readOpenState() {
    try {
      const raw = localStorage.getItem(OPEN_STATE_KEY);
      const obj = raw ? JSON.parse(raw) : {};
      return obj && typeof obj === "object" ? obj : {};
    } catch (e) {
      return {};
    }
  }

  function writeOpenState(state) {
    try {
      localStorage.setItem(OPEN_STATE_KEY, JSON.stringify(state || {}));
    } catch (e) {}
  }

  function defaultOpenByDepth(depth) {
    return depth <= 1;
  }

  function nodeOpenAttr(node, depth, state) {
    const key = String(node && node.id || "");
    if (key && Object.prototype.hasOwnProperty.call(state || {}, key)) return state[key] ? " open" : "";
    return defaultOpenByDepth(depth) ? " open" : "";
  }

  function sortNodes(nodes) {
    (nodes || []).sort(function (a, b) {
      return (Number(a.order || 0) - Number(b.order || 0)) || String(a.label || "").localeCompare(String(b.label || ""));
    });
    (nodes || []).forEach(function (n) { sortNodes(n.children || []); });
    return nodes;
  }

  function buildTree(items, options) {
    options = options || {};
    const active = options.activeProfile || {};
    const rootIp = options.remote_ip || active.remote_ip || "local";
    const deviceId = options.device_instance_id || active.device_instance_id || "device-unknown";
    const deviceLabel = options.device_label || active.device_label || active.system_hostname || deviceId;
    const root = {
      id: "ip:" + rootIp,
      label: "🌍 " + rootIp,
      sub: "remote_ip",
      status: "ok",
      children: []
    };
    const dev = child(root, {
      id: "dev:" + deviceId,
      label: "💻 " + deviceLabel,
      sub: deviceId,
      status: "ok",
      children: []
    });
    const profileMap = new Map();
    const directOrder = { "PC1": 1, "N1": 2, "HOST1": 3 };

    (items || []).forEach(function (it) {
      const cid = String(val(it, ["client_id", "clientId", "cid"], "?")).toUpperCase();
      const endpoint = String(val(it, ["endpoint"], "ext4"));
      const tabKind = String(val(it, ["tab_kind", "tabKind"], ""));
      const tabId = tabIdOfItem(it);
      const routeKey = routeKeyOfItem(it);
      const profileId = String(val(it, ["profile_instance_id", "chrome_profile_instance_id"], ""));
      const title = String((it.tab && it.tab.title) || it.title || "");
      const status = it.status || (it.online === false ? "offline" : "online");
      const node = {
        id: makeId("client", [cid, endpoint, routeKey || tabId]),
        label: (cid === "PC1" ? "🖥 " : cid === "N1" ? "🧩 " : cid === "HOST1" ? "🛠 " : "🧷 ") + cid + " #" + endpoint,
        sub: routeKey ? "url5:" + routeKey : (tabId ? "tab:" + tabId : (title || "")),
        status: status,
        item: it,
        order: directOrder[cid] || 10
      };
      if (cid === "PC1" || endpoint === "mypc" || tabKind === "pc-agent" || cid === "N1" || endpoint === "node-red" || cid === "HOST1") {
        child(dev, node);
        return;
      }
      const pfKey = profileId || "profile-unknown";
      const pfLabel = profileId ? ("👤 " + profileId) : "👤 미확정 프로필";
      const pfSub = profileId ? "chrome_profile_instance_id" : "profile_instance_id 없음";
      const pfStatus = profileId ? "ok" : "unknown";
      if (!profileMap.has(pfKey)) {
        profileMap.set(pfKey, child(dev, {
          id: "pf:" + pfKey,
          label: pfLabel,
          sub: pfSub,
          status: pfStatus,
          order: 5,
          children: []
        }));
      }
      child(profileMap.get(pfKey), node);
    });
    sortNodes(root.children);
    return root;
  }

  function renderNode(node, ctx, depth, openState) {
    ctx = ctx || {};
    depth = depth || 0;
    const hasChildren = node.children && node.children.length;
    const item = node.item || null;
    const info = item && ctx.statusInfoFromItem ? ctx.statusInfoFromItem(item) : { cls: node.status === "offline" ? "offline" : "online", emoji: node.status === "offline" ? "🔴" : "🟢", text: node.status || "ok" };
    const tabId = item && ctx.tabIdOfItem ? ctx.tabIdOfItem(item) : "";
    const routeKey = item && (ctx.routeKeyOfItem ? ctx.routeKeyOfItem(item) : routeKeyOfItem(item));
    const approved = item && ctx.isApprovedClient ? ctx.isApprovedClient(item) : false;
    const addr = item && ctx.makeAddr ? ctx.makeAddr(item) : "";
    const cleanTitle = item && ctx.shortCleanTitle ? ctx.shortCleanTitle(item) : "";
    const openAttr = nodeOpenAttr(node, depth, openState || {});
    let html = "";
    if (hasChildren) {
      html += "<details class='aiwk-map-node aiwk-map-depth-" + depth + "' data-node-id='" + esc(node.id || "") + "'" + openAttr + ">";
      html += "<summary><span class='aiwk-map-node-main'><span class='aiwk-map-node-label'>" + esc(node.label) + "</span>" + (node.sub ? "<span class='aiwk-map-node-sub'>" + esc(node.sub) + "</span>" : "") + "</span></summary>";
      html += "<div class='aiwk-map-children'>";
      (node.children || []).forEach(function (ch) { html += renderNode(ch, ctx, depth + 1, openState || {}); });
      html += "</div></details>";
      return html;
    }
    html += "<div class='aiwk-map-leaf client-line' data-addr='" + esc(addr) + "' data-client-id='" + esc(item ? item.client_id || "" : "") + "' data-tab-id='" + esc(tabId) + "'>";
    html += "<div class='client-top'>";
    html += ctx.statusPillHtml ? ctx.statusPillHtml(info) : "<span class='status-pill " + esc(info.cls || "unknown") + "'>" + esc(info.emoji || "⚫") + " " + esc(info.text || "") + "</span>";
    html += ctx.clientBadgeHtml ? ctx.clientBadgeHtml(item ? item.client_id || "?" : "?") : "<span class='client-badge'>" + esc(item ? item.client_id || "?" : "?") + "</span>";
    html += "<span class='tab-id-text'>url5:" + esc(routeKey || "-") + "</span>";
    html += "<span class='tab-id-text'>tab:" + esc(tabId || "-") + "</span>";
    html += "<span>" + esc(item ? item.endpoint || "ext4" : "") + "</span>";
    html += item && item.can_speak ? "<span class='ok'>token</span>" : "<span class='muted'>limited</span>";
    html += item && item.can_execute ? "<span class='warn'>exec</span>" : "";
    if (item) html += "<button type='button' class='client-approve-btn' data-action='toggle-approve'>" + (approved ? "승인해제" : "묶기/승인") + "</button>";
    html += "</div>";
    html += "<div class='small client-clean-title'>" + esc(cleanTitle || "-") + "</div>";
    html += "<div class='small client-addr-line'>" + esc(addr || "") + "</div>";
    html += "</div>";
    return html;
  }

  function bindLeafEvents(box, ctx) {
    if (!box || !ctx) return;
    box.querySelectorAll(".aiwk-map-leaf").forEach(function (el) {
      const cid = el.getAttribute("data-client-id") || "?";
      const tabId = el.getAttribute("data-tab-id") || "-";
      const addr = el.getAttribute("data-addr") || "";
      const item = ctx.findItem ? ctx.findItem(cid, tabId, addr) : null;
      el.addEventListener("click", function (ev) {
        if (ev.target && ev.target.getAttribute && ev.target.getAttribute("data-action") === "toggle-approve") return;
        if (addr && ctx.setToInput) ctx.setToInput(addr);
        const routeKey = item && (ctx.routeKeyOfItem ? ctx.routeKeyOfItem(item) : routeKeyOfItem(item));
        if (ctx.addChat) ctx.addChat({ kind: "system", text: "대상 선택: " + cid + " / url5:" + (routeKey || "-") + " / tab:" + tabId + (item && ctx.isApprovedClient && ctx.isApprovedClient(item) ? " / 승인됨" : " / 미승인·Ping 가능"), to: cid });
      });
      const btn = el.querySelector(".client-approve-btn");
      if (btn && item && ctx.setApprovedClient && ctx.renderAgain) {
        btn.addEventListener("click", function (ev) {
          ev.stopPropagation();
          const next = !(ctx.isApprovedClient && ctx.isApprovedClient(item));
          ctx.setApprovedClient(item, next);
          const routeKey = ctx.routeKeyOfItem ? ctx.routeKeyOfItem(item) : routeKeyOfItem(item);
          if (ctx.addChat) ctx.addChat({ kind: "system", text: (next ? "승인/묶기: " : "승인해제: ") + cid + " / url5:" + (routeKey || "-") + " / tab:" + tabId, statusEmoji: next ? "🟢" : "🟡" });
          ctx.renderAgain();
        });
      }
    });
  }

  function bindOpenStateEvents(box) {
    if (!box) return;
    const state = readOpenState();
    box.querySelectorAll("details.aiwk-map-node[data-node-id]").forEach(function (el) {
      el.addEventListener("toggle", function () {
        const key = el.getAttribute("data-node-id") || "";
        if (!key) return;
        state[key] = !!el.open;
        writeOpenState(state);
      });
    });
  }

  function renderHtmlTree(box, tree, ctx) {
    if (!box) return false;
    if (!tree) {
      box.textContent = "연결 지도 대기";
      return false;
    }
    const openState = readOpenState();
    box.innerHTML = "<div class='aiwk-map-tools'><span class='aiwk-map-version'>map " + VERSION + "</span><span class='aiwk-map-mode'>HTML tree fallback</span></div><div class='aiwk-map-tree'>" + renderNode(tree, ctx || {}, 0, openState) + "</div>";
    bindOpenStateEvents(box);
    bindLeafEvents(box, ctx || {});
    return true;
  }

  function render(box, items, ctx) {
    const tree = buildTree(items || [], ctx || {});
    return renderHtmlTree(box, tree, ctx || {});
  }

  window.AIWK_RELAY_MAP = {
    VERSION: VERSION,
    buildTree: buildTree,
    renderHtmlTree: renderHtmlTree,
    render: render
  };
}());
