var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
  var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
  for (var i = decorators.length - 1, decorator; i >= 0; i--)
    if (decorator = decorators[i])
      result = (kind ? decorator(target, key, result) : decorator(result)) || result;
  if (kind && result) __defProp(target, key, result);
  return result;
};
/**
 * @license
 * Copyright 2025-2026 Open Home Foundation
 * SPDX-License-Identifier: Apache-2.0
 */
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";
import {
  createBorderRouterIconDataUrl,
  createNodeIconDataUrl,
  createUnknownDeviceIconDataUrl
} from "../../util/device-icons.js";
import { BaseNetworkGraph } from "./base-network-graph.js";
import {
  buildExtAddrMap,
  buildRloc16Map,
  buildThreadEdgePairs,
  decodeMeshcopStateBitmap,
  findUnknownDevices,
  getDeviceName,
  getEdgeSignalScore,
  getNeighborTableLength,
  getNetworkType,
  getThreadExtendedAddressHex,
  getThreadRole,
  stripMdnsHostname
} from "./network-utils.js";
let ThreadGraph = class extends BaseNetworkGraph {
  constructor() {
    super(...arguments);
    this.borderRouters = /* @__PURE__ */ new Map();
    this.hideOfflineNodes = false;
    this.hideWeakSignalEdges = false;
    this.hideMediumSignalEdges = false;
    this.hideStrongSignalEdges = false;
    /** Cache of external Thread devices (Border Routers and unknown) for the current render */
    this._unknownDevices = [];
    /** Cached map of external Thread devices (rebuilt in _updateGraph) */
    this._unknownDevicesMapCache = /* @__PURE__ */ new Map();
    /** All computed edge pairs (rebuilt in _updateGraph) */
    this._edgePairs = /* @__PURE__ */ new Map();
    /** Base state of each edge after filter+dedup, before highlight */
    this._edgeBaseState = /* @__PURE__ */ new Map();
    /** Whether highlight is currently active */
    this._isHighlighted = false;
    /** Node ID currently highlighted (for icon restoration on clear) */
    this._highlightedNodeId = null;
  }
  /** Get external Thread devices as a map for use by details panel */
  get unknownDevicesMap() {
    return this._unknownDevicesMapCache;
  }
  /** Get computed edge pairs (for potential use by other components) */
  get edgePairs() {
    return this._edgePairs;
  }
  updated(changedProperties) {
    super.updated(changedProperties);
    if (changedProperties.has("hideOfflineNodes") || changedProperties.has("hideWeakSignalEdges") || changedProperties.has("hideMediumSignalEdges") || changedProperties.has("hideStrongSignalEdges") || changedProperties.has("borderRouters")) {
      this._debouncedUpdateGraph();
    }
  }
  /**
   * Searches for a Thread node (known or unknown) and selects it. Matches, in priority order:
   * 1. Extended address (EUI-64), accepting `AABBCCDDEEFF0011`, `AA:BB:...`, or `0x...` forms.
   * 2. Node id (exact).
   * 3. Visible device label (case-insensitive substring).
   * Returns true when a match is found.
   */
  selectBySearch(query) {
    const trimmed = query.trim();
    if (!trimmed) {
      return false;
    }
    const threadNodes = Object.values(this.nodes).filter(
      (node) => getNetworkType(node) === "thread" && !(this.hideOfflineNodes && node.available === false)
    );
    const normalized = normalizeExtendedAddressInput(trimmed);
    if (normalized) {
      for (const node of threadNodes) {
        const extAddressHex = getThreadExtendedAddressHex(node);
        if (extAddressHex && extAddressHex === normalized) {
          this.selectNode(String(node.node_id));
          return true;
        }
      }
      for (const [unknownId, unknown] of this._unknownDevicesMapCache) {
        if (normalizeExtendedAddressInput(unknown.extAddressHex) === normalized) {
          this.selectNode(unknownId);
          return true;
        }
      }
    }
    for (const node of threadNodes) {
      if (String(node.node_id) === trimmed) {
        this.selectNode(String(node.node_id));
        return true;
      }
    }
    const needle = trimmed.toLowerCase();
    for (const node of threadNodes) {
      if (getDeviceName(node).toLowerCase().includes(needle)) {
        this.selectNode(String(node.node_id));
        return true;
      }
    }
    return false;
  }
  _updateGraph() {
    if (!this._nodesDataSet || !this._edgesDataSet) return;
    this._clearOriginalEdgeColors();
    this._edgeBaseState.clear();
    this._isHighlighted = false;
    const threadNodes = Object.values(this.nodes).filter((node) => getNetworkType(node) === "thread");
    if (threadNodes.length === 0) {
      this._nodesDataSet.clear();
      this._edgesDataSet.clear();
      this._unknownDevices = [];
      this._unknownDevicesMapCache.clear();
      this._edgePairs.clear();
      return;
    }
    const extAddrMap = buildExtAddrMap(this.nodes);
    const rloc16Map = buildRloc16Map(this.nodes);
    this._unknownDevices = findUnknownDevices(this.nodes, extAddrMap, rloc16Map, this.borderRouters);
    this._unknownDevicesMapCache.clear();
    for (const device of this._unknownDevices) {
      this._unknownDevicesMapCache.set(device.id, device);
    }
    this._edgePairs = buildThreadEdgePairs(this.nodes, extAddrMap, rloc16Map, this._unknownDevices);
    const hiddenNodeIds = /* @__PURE__ */ new Set();
    const graphNodes = threadNodes.map((node) => {
      const nodeId = String(node.node_id);
      const threadRole = getThreadRole(node);
      const isOffline = node.available === false;
      const shouldHide = this.hideOfflineNodes && isOffline;
      if (shouldHide) {
        hiddenNodeIds.add(nodeId);
      }
      return {
        id: nodeId,
        label: getDeviceName(node),
        image: createNodeIconDataUrl(node, threadRole, false, isOffline),
        shape: "image",
        networkType: "thread",
        threadRole,
        offline: isOffline,
        hidden: shouldHide
      };
    });
    for (const device of this._unknownDevices) {
      const isSelected = device.id === this._selectedNodeId;
      const hasOnlineObserver = device.seenBy.some((nodeId) => {
        const node = this.nodes[nodeId];
        return node !== void 0 && node.available !== false;
      });
      let shouldHide;
      if (device.kind === "unknown") {
        shouldHide = !hasOnlineObserver;
        if (!shouldHide && device.seenBy.length === 1) {
          const observer = this.nodes[device.seenBy[0]];
          if (observer !== void 0 && getNeighborTableLength(observer) > 1) {
            shouldHide = true;
          }
        }
      } else {
        shouldHide = this.hideOfflineNodes && !hasOnlineObserver;
      }
      if (shouldHide) {
        hiddenNodeIds.add(device.id);
      }
      if (device.kind === "br") {
        const hostname = device.hostname !== void 0 ? stripMdnsHostname(device.hostname) : void 0;
        const top = (hostname ?? device.networkName ?? "Border Router").slice(0, 24);
        const suffix = hostname !== void 0 && device.networkName !== void 0 && device.networkName !== top ? `
${device.networkName}` : "";
        const label = `${top}${suffix}`;
        const decodedState = decodeMeshcopStateBitmap(device.stateBitmapHex);
        const isLeader = decodedState?.threadRoleValue === 3;
        const isPrimaryBbr = decodedState?.bbr === true && decodedState.bbrFunction === "primary";
        graphNodes.push({
          id: device.id,
          label,
          image: createBorderRouterIconDataUrl(isSelected, isLeader, isPrimaryBbr),
          shape: "image",
          networkType: "thread",
          isUnknown: false,
          hidden: shouldHide
        });
      } else {
        const typeLabel = device.isRouter ? "External Router" : "External Device";
        const suffix = device.networkName !== void 0 ? `
${device.networkName}` : "";
        graphNodes.push({
          id: device.id,
          label: `${typeLabel} (${device.extAddressHex.slice(-8)})${suffix}`,
          image: createUnknownDeviceIconDataUrl(device.isRouter, isSelected),
          shape: "image",
          networkType: "thread",
          isUnknown: true,
          hidden: shouldHide
        });
      }
    }
    const graphEdges = [];
    for (const pair of this._edgePairs.values()) {
      const edgesInPair = [];
      let hadZeroEdge = false;
      let hadLiveEdge = false;
      for (const conn of [pair.edgeAB, pair.edgeBA]) {
        if (!conn) continue;
        const fromId = String(conn.fromNodeId);
        const toId = String(conn.toNodeId);
        const isToUnknown = toId.startsWith("unknown_") || toId.startsWith("br_");
        const fromNode = this.nodes[fromId];
        const toNode = this.nodes[toId];
        const hasOfflineEndpoint = fromNode?.available === false || toNode?.available === false;
        let filterHidden = false;
        if (hiddenNodeIds.has(fromId) || hiddenNodeIds.has(toId)) {
          filterHidden = true;
        }
        if (conn.signalLevel === "none") {
          filterHidden = true;
          hadZeroEdge = true;
        } else {
          hadLiveEdge = true;
        }
        if (!filterHidden && this.hideWeakSignalEdges && conn.signalLevel === "weak") {
          filterHidden = true;
        }
        if (!filterHidden && this.hideMediumSignalEdges && conn.signalLevel === "medium") {
          filterHidden = true;
        }
        if (!filterHidden && this.hideStrongSignalEdges && conn.signalLevel === "strong") {
          filterHidden = true;
        }
        const edgeId = `edge_${fromId}_${toId}`;
        const visEdge = {
          id: edgeId,
          from: fromId,
          to: toId,
          color: { color: conn.signalColor, highlight: conn.signalColor },
          width: 2,
          title: conn.rssi !== null ? `RSSI: ${conn.rssi} dBm, LQI: ${conn.lqi}` : `LQI: ${conn.lqi}`,
          dashes: isToUnknown || hasOfflineEndpoint,
          hidden: filterHidden,
          pairKey: pair.pairKey,
          reportingNodeId: fromId
        };
        edgesInPair.push({ conn, visEdge, filterHidden });
      }
      const visibleInPair = edgesInPair.filter((e) => !e.visEdge.hidden);
      if (visibleInPair.length > 1) {
        visibleInPair.sort((a, b) => getEdgeSignalScore(a.conn) - getEdgeSignalScore(b.conn));
        for (let i = 1; i < visibleInPair.length; i++) {
          visibleInPair[i].visEdge.hidden = true;
        }
      }
      if (hadZeroEdge && hadLiveEdge) {
        for (const e of edgesInPair) {
          if (!e.visEdge.hidden) {
            e.visEdge.dashes = true;
            e.visEdge.title = `${e.visEdge.title ?? ""} (asymmetric: peer reports no link)`;
          }
        }
      }
      for (const e of edgesInPair) {
        const isHidden = e.visEdge.hidden ?? false;
        let hiddenReason = "visible";
        if (isHidden) {
          hiddenReason = e.filterHidden ? "filter" : "dedup";
        }
        this._edgeBaseState.set(e.visEdge.id, {
          hiddenReason,
          width: e.visEdge.width,
          color: { color: e.visEdge.color.color, highlight: e.visEdge.color.highlight },
          dashes: e.visEdge.dashes ?? false
        });
        graphEdges.push(e.visEdge);
      }
    }
    const existingNodeIds = this._nodesDataSet.getIds();
    const newNodeIds = new Set(graphNodes.map((n) => n.id));
    const nodesToRemove = existingNodeIds.filter((id) => !newNodeIds.has(id));
    if (nodesToRemove.length > 0) {
      this._nodesDataSet.remove(nodesToRemove);
    }
    this._nodesDataSet.update(graphNodes);
    this._edgesDataSet.clear();
    this._edgesDataSet.add(graphEdges);
    if (this._selectedNodeId !== null) {
      this._highlightConnections(this._selectedNodeId);
    }
  }
  /**
   * Highlights edges connected to the selected node with swap/arrow logic.
   *
   * For each visible edge connected to the highlighted node:
   * - If the visible edge comes from the remote node AND there is a
   *   dedup-hidden edge from the highlighted node → SWAP: show the
   *   highlighted node's edge instead (its signal is better or equal,
   *   but we prefer the highlighted node's perspective).
   * - Otherwise → thicken the edge. If the pair is truly one-way (only
   *   one of edgeAB/edgeBA exists), also draw an arrow in the data
   *   direction so asymmetric visibility is visible at a glance.
   */
  _highlightConnections(nodeId) {
    if (!this._edgesDataSet || !this._nodesDataSet) return;
    const nodeIdStr = String(nodeId);
    if (this._isHighlighted && this._highlightedNodeId === nodeIdStr) return;
    if (this._isHighlighted) {
      this._restoreEdgeBaseState();
    }
    const allEdges = this._edgesDataSet.get();
    const dimmedColor = this._getDimmedEdgeColor();
    const edgesByPair = /* @__PURE__ */ new Map();
    for (const edge of allEdges) {
      if (edge.pairKey) {
        const list = edgesByPair.get(edge.pairKey) ?? [];
        list.push(edge);
        edgesByPair.set(edge.pairKey, list);
      }
    }
    const connectedNodeIds = /* @__PURE__ */ new Set();
    const edgeUpdates = {};
    for (const edge of allEdges) {
      const fromStr = String(edge.from);
      const toStr = String(edge.to);
      const isConnected = fromStr === nodeIdStr || toStr === nodeIdStr;
      if (!isConnected) {
        const baseState2 = this._edgeBaseState.get(edge.id);
        if (baseState2 && baseState2.hiddenReason === "visible") {
          edgeUpdates[String(edge.id)] = {
            id: edge.id,
            width: 1,
            color: { color: dimmedColor, highlight: dimmedColor }
          };
        }
        continue;
      }
      const baseState = this._edgeBaseState.get(edge.id);
      if (!baseState || baseState.hiddenReason !== "visible") {
        continue;
      }
      const remoteId = fromStr === nodeIdStr ? toStr : fromStr;
      const reportingId = String(edge.reportingNodeId ?? edge.from);
      connectedNodeIds.add(remoteId);
      if (reportingId !== nodeIdStr) {
        const pairEdges = edge.pairKey ? edgesByPair.get(edge.pairKey) : void 0;
        const swapCandidate = pairEdges?.find((e) => {
          const rid = String(e.reportingNodeId ?? e.from);
          if (rid !== nodeIdStr) return false;
          const bs = this._edgeBaseState.get(e.id);
          return bs?.hiddenReason === "dedup";
        });
        if (swapCandidate) {
          const swapBaseState = this._edgeBaseState.get(swapCandidate.id);
          edgeUpdates[String(edge.id)] = {
            id: edge.id,
            hidden: true
          };
          edgeUpdates[String(swapCandidate.id)] = {
            id: swapCandidate.id,
            hidden: false,
            width: 3,
            color: swapBaseState ? { color: swapBaseState.color.color, highlight: swapBaseState.color.highlight } : { color: "#999999", highlight: "#999999" }
          };
        } else {
          edgeUpdates[String(edge.id)] = this._reverseEdgeUpdate(edge);
        }
      } else {
        edgeUpdates[String(edge.id)] = this._asymmetricEdgeUpdate(edge);
      }
    }
    this._edgesDataSet.update(Object.values(edgeUpdates));
    const allNodes = this._nodesDataSet.get();
    const nodeUpdates = allNodes.map((node) => {
      const isNeighbor = connectedNodeIds.has(node.id);
      const isSelected = node.id === nodeId;
      return {
        id: node.id,
        size: isSelected ? 40 : isNeighbor ? 35 : 25,
        font: {
          size: isSelected ? 14 : isNeighbor ? 13 : 11,
          color: this._getFontColor(),
          bold: isSelected || isNeighbor ? { color: this._getFontColor() } : void 0
        },
        opacity: isSelected || isNeighbor ? 1 : 0.5
      };
    });
    this._nodesDataSet.update(nodeUpdates);
    if (this._highlightedNodeId && this._highlightedNodeId !== nodeIdStr) {
      this._setNodeIconHighlight(this._highlightedNodeId, false);
    }
    this._highlightedNodeId = nodeIdStr;
    this._setNodeIconHighlight(nodeIdStr, true);
    this._isHighlighted = true;
  }
  /**
   * Thicken a connected edge and add a directional arrow when the pair is
   * truly one-way in data (only one of edgeAB/edgeBA exists). Used for the
   * highlighted-reports-peer branch where filter-hidden peer directions
   * should not draw an arrow on the user's own perspective.
   *
   * Explicit arrow object (vs the shorthand "to") keeps the head visible
   * on dashed offline edges where some vis.js builds skip the shorthand.
   */
  _asymmetricEdgeUpdate(edge) {
    const pair = edge.pairKey ? this._edgePairs.get(edge.pairKey) : void 0;
    const isAsymmetric = pair ? !pair.edgeAB || !pair.edgeBA : false;
    const update = {
      id: edge.id,
      width: 3
    };
    if (isAsymmetric) {
      update.arrows = { to: { enabled: true, scaleFactor: 1 } };
    }
    return update;
  }
  /**
   * Thicken a connected edge and always add a directional arrow. Used for
   * the remote-reports-highlighted branch where the displayed edge is the
   * peer's direction (no swap target available); the user sees the same
   * single line whether the outgoing direction is filter-hidden or absent.
   */
  _reverseEdgeUpdate(edge) {
    return {
      id: edge.id,
      width: 3,
      arrows: { to: { enabled: true, scaleFactor: 1 } }
    };
  }
  /**
   * Swap a node's icon between the default and highlighted variants.
   * Kept separate so both `_highlightConnections` (switching target) and
   * `_clearHighlights` (fully unselecting) reach the same end state.
   */
  _setNodeIconHighlight(nodeId, isHighlighted) {
    if (!this._nodesDataSet) return;
    const nodeData = this.nodes[nodeId];
    if (nodeData) {
      const threadRole = getThreadRole(nodeData);
      const isOffline = nodeData.available === false;
      this._nodesDataSet.update({
        id: nodeId,
        image: createNodeIconDataUrl(nodeData, threadRole, isHighlighted, isOffline)
      });
      return;
    }
    const external = this._unknownDevicesMapCache.get(nodeId);
    if (external?.kind === "br") {
      const decodedState = decodeMeshcopStateBitmap(external.stateBitmapHex);
      const isLeader = decodedState?.threadRoleValue === 3;
      const isPrimaryBbr = decodedState?.bbr === true && decodedState.bbrFunction === "primary";
      this._nodesDataSet.update({
        id: nodeId,
        image: createBorderRouterIconDataUrl(isHighlighted, isLeader, isPrimaryBbr)
      });
    } else if (nodeId.startsWith("unknown_") || nodeId.startsWith("br_")) {
      this._nodesDataSet.update({
        id: nodeId,
        image: createUnknownDeviceIconDataUrl(external?.isRouter ?? false, isHighlighted)
      });
    }
  }
  /**
   * Clears all highlights and restores the graph to its base state
   * (after filter+dedup, before highlight modifications).
   */
  _clearHighlights() {
    if (!this._edgesDataSet || !this._nodesDataSet) return;
    this._restoreEdgeBaseState();
    if (this._highlightedNodeId) {
      this._setNodeIconHighlight(this._highlightedNodeId, false);
      this._highlightedNodeId = null;
    }
    const allNodes = this._nodesDataSet.get();
    const nodeUpdates = allNodes.map((node) => ({
      id: node.id,
      size: 30,
      font: {
        size: 12,
        color: this._getFontColor(),
        bold: void 0
      },
      opacity: 1
    }));
    this._nodesDataSet.update(nodeUpdates);
    this._isHighlighted = false;
  }
  /**
   * Restores all edges to their base state (undoes highlight modifications).
   * This resets hidden/visible state, width, color, and dashes.
   */
  _restoreEdgeBaseState() {
    if (!this._edgesDataSet) return;
    const edgeUpdates = [];
    for (const [id, baseState] of this._edgeBaseState) {
      edgeUpdates.push({
        id,
        hidden: baseState.hiddenReason !== "visible",
        width: baseState.width,
        color: { color: baseState.color.color, highlight: baseState.color.highlight },
        dashes: baseState.dashes,
        arrows: ""
      });
    }
    this._edgesDataSet.update(edgeUpdates);
  }
  render() {
    const threadNodes = Object.values(this.nodes).filter((node) => getNetworkType(node) === "thread");
    const visibleThreadNodes = this.hideOfflineNodes ? threadNodes.filter((node) => node.available !== false) : threadNodes;
    if (visibleThreadNodes.length === 0) {
      const allOfflineFiltered = threadNodes.length > 0 && this.hideOfflineNodes;
      return html`
                <div class="empty-state">
                    <p>${allOfflineFiltered ? "No online Thread devices" : "No Thread devices found"}</p>
                    <p class="hint">
                        ${allOfflineFiltered ? 'Disable the "Offline nodes" filter to show offline devices' : "Thread devices will appear here once commissioned"}
                    </p>
                </div>
            `;
    }
    return html` <div class="graph-container"></div> `;
  }
};
__decorateClass([
  property({ attribute: false })
], ThreadGraph.prototype, "borderRouters", 2);
__decorateClass([
  property({ type: Boolean })
], ThreadGraph.prototype, "hideOfflineNodes", 2);
__decorateClass([
  property({ type: Boolean })
], ThreadGraph.prototype, "hideWeakSignalEdges", 2);
__decorateClass([
  property({ type: Boolean })
], ThreadGraph.prototype, "hideMediumSignalEdges", 2);
__decorateClass([
  property({ type: Boolean })
], ThreadGraph.prototype, "hideStrongSignalEdges", 2);
ThreadGraph = __decorateClass([
  customElement("thread-graph")
], ThreadGraph);
function normalizeExtendedAddressInput(address) {
  const trimmed = address.trim();
  if (!trimmed) {
    return null;
  }
  const noPrefix = trimmed.startsWith("0x") || trimmed.startsWith("0X") ? trimmed.slice(2) : trimmed;
  const hexOnly = noPrefix.replace(/[^a-fA-F0-9]/g, "");
  if (hexOnly.length !== 16 || !/^[a-fA-F0-9]{16}$/.test(hexOnly)) {
    return null;
  }
  return hexOnly.toUpperCase();
}
export {
  ThreadGraph
};
//# sourceMappingURL=thread-graph.js.map
