/**
 * @license
 * Copyright 2025-2026 Open Home Foundation
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  MatterError,
  Diagnostic,
  Bytes,
  camelize,
  ClusterId,
  FabricIndex,
  Logger,
  LogLevel,
  Millis,
  NodeId,
  ObserverGroup
} from "@matter/main";
import { EndpointNumber, QrPairingCodeCodec } from "@matter/main/types";
import { NodeStates } from "@project-chip/matter.js/device";
import { WebSocketServer } from "ws";
import { TestNodeCommandHandler } from "../controller/TestNodeCommandHandler.js";
import { VendorIds } from "../data/VendorIDs.js";
import { ClusterMap } from "../model/ModelMapper.js";
import {
  ServerError,
  ServerErrorCode
} from "../types/WebSocketMessageTypes.js";
import { formatNodeId } from "../util/formatNodeId.js";
import { MATTER_VERSION } from "../util/matterVersion.js";
import {
  convertMatterToWebSocketNameBased,
  convertMatterToWebSocketTagBased,
  parseBigIntAwareJson,
  splitAttributePath,
  toBigIntAwareJson
} from "./Converters.js";
const logger = Logger.get("WebSocketControllerHandler");
const MAX_COMMISSION_NODE_ID_ATTEMPTS = 5;
function isIdentityConflict(error) {
  for (let current = error; current instanceof Error; current = current.cause) {
    if (current instanceof MatterError && current.id === "identity-conflict") {
      return true;
    }
  }
  return false;
}
const EVENT_HISTORY_SIZE = 25;
let connectionIdCounter = 0;
function generateConnectionId() {
  const id = connectionIdCounter;
  connectionIdCounter = connectionIdCounter + 1 & 65535;
  return id.toString(16);
}
const SCHEMA_VERSION = 11;
const skipMessageContentInLogFor = ["start_listening"];
class WebSocketControllerHandler {
  #controller;
  #commandHandler;
  #testNodeHandler;
  #config;
  #serverVersion;
  #wss;
  #closed = false;
  #shuttingDown = false;
  #serverObservers = new ObserverGroup();
  /** Upgrade listener removers, one per HTTP server `register()` call (multi-bind). */
  #removeUpgradeListeners = [];
  /** Circular buffer for recent node events (max 25) */
  #eventHistory = [];
  /** Track when each node was last interviewed (connected) - keyed by nodeId */
  #lastInterviewDates = /* @__PURE__ */ new Map();
  constructor(controller, config, serverVersion) {
    this.#controller = controller;
    this.#commandHandler = controller.commandHandler;
    this.#testNodeHandler = new TestNodeCommandHandler();
    this.#config = config;
    this.#serverVersion = serverVersion;
  }
  /**
   * Health check information for the server.
   * Returns the server version and number of registered nodes.
   */
  health() {
    const realNodeCount = this.#commandHandler.getNodeIds().length;
    const testNodeCount = this.#testNodeHandler.getNodes().length;
    return {
      version: this.#serverVersion,
      node_count: realNodeCount + testNodeCount
    };
  }
  /**
   * Get the appropriate command handler for a node ID.
   * Returns TestNodeCommandHandler for test nodes, ControllerCommandHandler for real nodes.
   */
  #handlerFor(nodeId) {
    return TestNodeCommandHandler.isTestNodeId(nodeId) ? this.#testNodeHandler : this.#commandHandler;
  }
  /**
   * Add an event to the history buffer, maintaining max size.
   */
  #addEventToHistory(event) {
    this.#eventHistory.push(event);
    if (this.#eventHistory.length > EVENT_HISTORY_SIZE) {
      this.#eventHistory.shift();
    }
  }
  /**
   * Get the event history (last 25 events).
   */
  getEventHistory() {
    return [...this.#eventHistory];
  }
  initiateShutdown() {
    this.#shuttingDown = true;
  }
  async register(server) {
    logger.notice(`Starting server: matter-server/${this.#serverVersion} (matter.js/${MATTER_VERSION})`);
    const isFirstBind = this.#wss === void 0;
    const wss = this.#wss ??= new WebSocketServer({ noServer: true });
    const upgradeHandler = (req, socket, head) => {
      const path = req.url?.split("?")[0];
      if (path === "/ws") {
        req._matterHandledUpgrade = true;
        wss.handleUpgrade(
          req,
          socket,
          head,
          (ws) => wss.emit("connection", ws, req)
        );
      }
    };
    server.on("upgrade", upgradeHandler);
    this.#removeUpgradeListeners.push(() => server.removeListener("upgrade", upgradeHandler));
    if (!isFirstBind) {
      return;
    }
    this.#serverObservers.on(this.#commandHandler.events.webRtcCallback, (data) => {
      this.#broadcastEvent("webrtc_callback", data);
    });
    wss.on("connection", (ws) => {
      if (this.#closed || this.#shuttingDown) {
        try {
          ws.close(1001, "server shutting down");
        } catch {
        }
        return;
      }
      const connId = generateConnectionId();
      logger.info(`[${connId}] WebSocket connection established`);
      let listening = false;
      const observers = new ObserverGroup();
      const sendNodeFullDetails = (eventName, nodeId) => {
        try {
          const nodeDetails = this.#collectNodeDetails(nodeId);
          logger.debug(
            `[${connId}] Sending ${eventName} event for Node ${this.#commandHandler.formatNode(nodeId)}`
          );
          ws.send(toBigIntAwareJson({ event: eventName, data: nodeDetails }));
        } catch (err) {
          logger.error(
            `[${connId}] Failed to collect node details for Node ${this.#commandHandler.formatNode(nodeId)}`,
            err
          );
        }
      };
      const pendingNodeUpdated = /* @__PURE__ */ new Set();
      let nodeUpdatedFlushScheduled = false;
      const flushNodeUpdated = () => {
        nodeUpdatedFlushScheduled = false;
        const nodeIds = [...pendingNodeUpdated];
        pendingNodeUpdated.clear();
        if (this.#closed || this.#shuttingDown || !listening || connectionClosed) return;
        for (const nodeId of nodeIds) {
          if (!this.#commandHandler.hasNode(nodeId)) continue;
          sendNodeFullDetails("node_updated", nodeId);
        }
      };
      const sendNodeDetailsEvent = (eventName, nodeId) => {
        if (this.#closed || this.#shuttingDown || !listening) return;
        switch (eventName) {
          case "node_added":
            pendingNodeUpdated.delete(nodeId);
            sendNodeFullDetails("node_added", nodeId);
            break;
          case "node_updated":
            pendingNodeUpdated.add(nodeId);
            if (!nodeUpdatedFlushScheduled) {
              nodeUpdatedFlushScheduled = true;
              setImmediate(flushNodeUpdated);
            }
            break;
          case "node_removed":
            pendingNodeUpdated.delete(nodeId);
            logger.debug(
              `[${connId}] Sending node_removed event for Node ${this.#commandHandler.formatNode(nodeId)}`
            );
            ws.send(toBigIntAwareJson({ event: eventName, data: nodeId }));
            break;
        }
      };
      observers.on(this.#commandHandler.events.attributeChanged, (nodeId, data) => {
        if (this.#closed || this.#shuttingDown || !listening) return;
        const { endpointId, clusterId, attributeId } = data.path;
        const pathStr = `${endpointId}/${clusterId}/${attributeId}`;
        const clusterData = ClusterMap[clusterId];
        const value = convertMatterToWebSocketTagBased(
          data.value,
          clusterData?.attributes[attributeId],
          clusterData?.model
        );
        logger.debug(
          `[${connId}] Sending attribute_updated event for Node ${this.#commandHandler.formatNode(nodeId)}`,
          pathStr,
          value
        );
        ws.send(toBigIntAwareJson({ event: "attribute_updated", data: [nodeId, pathStr, value] }));
      });
      observers.on(this.#commandHandler.events.eventChanged, (nodeId, data) => {
        if (this.#closed || this.#shuttingDown || !listening) return;
        const { path, events } = data;
        const { endpointId, clusterId, eventId } = path;
        const clusterData = ClusterMap[clusterId];
        for (const event of events) {
          let timestamp;
          let timestampType;
          if (event.epochTimestamp !== void 0) {
            timestamp = event.epochTimestamp;
            timestampType = 1;
          } else if (event.systemTimestamp !== void 0) {
            timestamp = event.systemTimestamp;
            timestampType = 0;
          } else {
            timestamp = Date.now();
            timestampType = 2;
          }
          const eventModel = clusterData?.events[eventId];
          const convertedData = event.data !== void 0 ? convertMatterToWebSocketNameBased(event.data, eventModel, clusterData?.model) : null;
          const nodeEvent = {
            node_id: nodeId,
            endpoint_id: endpointId,
            cluster_id: clusterId,
            event_id: eventId,
            event_number: event.eventNumber,
            priority: event.priority,
            timestamp,
            timestamp_type: timestampType,
            data: convertedData
          };
          this.#addEventToHistory(nodeEvent);
          logger.debug(
            `[${connId}] Sending node_event for Node ${this.#commandHandler.formatNode(nodeId)}`,
            nodeEvent
          );
          ws.send(toBigIntAwareJson({ event: "node_event", data: nodeEvent }));
        }
      });
      observers.on(this.#commandHandler.events.nodeAdded, (nodeId) => {
        sendNodeDetailsEvent("node_added", nodeId);
      });
      observers.on(this.#commandHandler.events.nodeStateChanged, (nodeId, state) => {
        if (state === NodeStates.Connected) {
          this.#lastInterviewDates.set(nodeId, /* @__PURE__ */ new Date());
        }
      });
      observers.on(this.#commandHandler.events.nodeAvailabilityChanged, (nodeId, available) => {
        logger.info(
          `[${connId}] Node ${this.#commandHandler.formatNode(nodeId)} availability changed to ${available}`
        );
        sendNodeDetailsEvent("node_updated", nodeId);
      });
      observers.on(this.#commandHandler.events.nodeStructureChanged, (nodeId) => {
        sendNodeDetailsEvent("node_updated", nodeId);
      });
      observers.on(this.#commandHandler.events.nodeDecommissioned, (nodeId) => {
        sendNodeDetailsEvent("node_removed", nodeId);
      });
      observers.on(this.#commandHandler.events.nodeEndpointAdded, (nodeId, endpointId) => {
        if (this.#closed || this.#shuttingDown || !listening) return;
        logger.info(
          `[${connId}] Sending endpoint_added event for Node ${this.#commandHandler.formatNode(nodeId)} endpoint ${endpointId}`
        );
        ws.send(
          toBigIntAwareJson({ event: "endpoint_added", data: { node_id: nodeId, endpoint_id: endpointId } })
        );
      });
      observers.on(this.#commandHandler.events.nodeEndpointRemoved, (nodeId, endpointId) => {
        if (this.#closed || this.#shuttingDown || !listening) return;
        logger.info(
          `[${connId}] Sending endpoint_removed event for Node ${this.#commandHandler.formatNode(nodeId)} endpoint ${endpointId}`
        );
        ws.send(
          toBigIntAwareJson({
            event: "endpoint_removed",
            data: { node_id: nodeId, endpoint_id: endpointId }
          })
        );
      });
      observers.on(this.#testNodeHandler.nodeAdded, (_nodeId, testNode) => {
        if (this.#closed || this.#shuttingDown || !listening) return;
        logger.info(`[${connId}] Sending node_added event for test node ${testNode.node_id}`);
        ws.send(toBigIntAwareJson({ event: "node_added", data: testNode }));
      });
      observers.on(this.#testNodeHandler.nodeRemoved, (nodeId) => {
        if (this.#closed || this.#shuttingDown || !listening) return;
        logger.info(`[${connId}] Sending node_removed event for test node ${formatNodeId(nodeId)}`);
        ws.send(toBigIntAwareJson({ event: "node_removed", data: nodeId }));
      });
      let connectionClosed = false;
      const onClose = () => {
        if (connectionClosed) {
          return;
        }
        connectionClosed = true;
        pendingNodeUpdated.clear();
        logger.info(`[${connId}] WebSocket connection closed`);
        observers.close();
      };
      ws.on(
        "message",
        (data) => void this.#handleWebSocketRequest(connId, data.toString()).then(
          ({ response, enableListeners }) => {
            if (this.#closed) return;
            if (enableListeners) {
              listening = true;
            }
            ws.send(toBigIntAwareJson(response));
          },
          (err) => logger.error(`[${connId}] WebSocket request error`, err)
        )
      );
      ws.on("close", onClose);
      ws.on("error", (err) => {
        logger.error(`[${connId}] WebSocket error`, err);
        onClose();
      });
      this.#getServerInfo().then(
        (response) => {
          logger.debug(`[${connId}] Sending server info`);
          ws.send(toBigIntAwareJson(response));
        },
        (err) => logger.error(`[${connId}] WebSocket handshake error`, err)
      );
    });
    await this.#commandHandler.initializeNodes();
  }
  unregister() {
    if (!this.#wss || this.#closed) {
      return Promise.resolve();
    }
    this.#closed = true;
    this.#serverObservers.close();
    for (const remove of this.#removeUpgradeListeners) remove();
    this.#removeUpgradeListeners = [];
    const shutdownMessage = toBigIntAwareJson({ event: "server_shutdown", data: {} });
    this.#wss.clients.forEach((client) => {
      if (client.readyState === 1) {
        try {
          client.send(shutdownMessage, () => {
            client.close();
          });
        } catch (err) {
          logger.warn("Failed to send server_shutdown event to client", err);
        }
      }
    });
    const wss = this.#wss;
    return new Promise((resolve, reject) => {
      wss.close((err) => {
        if (err) {
          reject(err);
        } else {
          resolve();
        }
      });
    });
  }
  async #handleWebSocketRequest(connId, data) {
    let messageId;
    let command;
    try {
      logger.debug(`[${connId}] WebSocket request`, () => data);
      const request = parseBigIntAwareJson(data);
      const { args } = request;
      messageId = request.message_id;
      command = request.command;
      let result;
      let enableListeners = void 0;
      switch (command) {
        case "start_listening":
          result = this.#handleStartListening(args);
          enableListeners = true;
          break;
        case "set_default_fabric_label":
          result = await this.#handleSetDefaultFabricLabel(args);
          break;
        case "commission_with_code":
          result = await this.#handleCommissionWithCode(args);
          break;
        case "commission_on_network":
          result = await this.#handleCommissionOnNetwork(args);
          break;
        case "get_node":
          result = await this.#handleGetNode(args);
          break;
        case "get_nodes":
          result = this.#handleGetNodes(args);
          break;
        case "get_node_ip_addresses":
          result = await this.#handleGetNodeIpAddresses(args);
          break;
        case "read_attribute":
          result = await this.#handleReadAttribute(args);
          break;
        case "get_vendor_names":
          result = await this.#handleGetVendorNames(args);
          break;
        case "device_command":
          result = await this.#handleDeviceCommand(args);
          break;
        case "send_webrtc_provider_command":
          result = await this.#handleSendWebRtcProviderCommand(args);
          break;
        case "write_attribute":
          result = await this.#handleWriteAttribute(args);
          break;
        case "interview_node":
          result = await this.#handleInterviewNode(args);
          break;
        case "ping_node":
          result = await this.#handlePingNode(args);
          break;
        case "diagnostics":
          result = {
            info: await this.#getServerInfo(),
            nodes: this.#handleGetNodes(args),
            events: this.getEventHistory()
          };
          break;
        case "remove_node":
          result = await this.#handleRemoveNode(args);
          break;
        case "set_wifi_credentials":
          result = await this.#handleSetWifiCredentials(args);
          break;
        case "set_thread_dataset":
          result = await this.#handleSetThreadDataset(args);
          break;
        case "remove_wifi_credentials":
          result = await this.#handleRemoveWifiCredentials();
          break;
        case "remove_thread_dataset":
          result = await this.#handleRemoveThreadDataset();
          break;
        case "get_thread_border_routers":
          result = this.#controller.borderRouters.list();
          break;
        case "open_commissioning_window":
          result = await this.#handleOpenCommissioningWindow(args);
          break;
        case "discover_commissionable_nodes":
          result = await this.#handleDiscoverCommissionableNodes(args);
          break;
        case "get_matter_fabrics":
          result = await this.#handleGetMatterFabrics(args);
          break;
        case "remove_matter_fabric":
          result = await this.#handleRemoveMatterFabric(args);
          break;
        case "set_acl_entry":
          result = await this.#handleSetAclEntry(args);
          break;
        case "set_node_binding":
          result = await this.#handleSetNodeBinding(args);
          break;
        case "import_test_node":
          result = await this.#handleImportTestNode(args);
          break;
        case "check_node_update":
          result = await this.#handleCheckNodeUpdate(args);
          break;
        case "update_node":
          result = await this.#handleUpdateNode(args);
          break;
        case "server_info":
          result = await this.#getServerInfo();
          break;
        case "discover":
          result = await this.#handleDiscoverCommissionableNodes({});
          break;
        case "get_loglevel":
          result = this.#handleGetLogLevel();
          break;
        case "set_loglevel":
          result = this.#handleSetLogLevel(args);
          break;
        default:
          throw ServerError.invalidCommand(command);
      }
      if (result === void 0) {
        throw ServerError.sdkStackError("Command handler returned no response");
      }
      if (skipMessageContentInLogFor.includes(command)) {
        logger.debug(`[${connId}] WebSocket response (${command})`, messageId);
      } else {
        logger.debug(`[${connId}] WebSocket response (${command})`, messageId, result);
      }
      return {
        response: {
          message_id: messageId ?? "",
          result
        },
        enableListeners
      };
    } catch (err) {
      logger.error(`[${connId}] WebSocket error response (${command})`, messageId, err);
      const errorCode = err instanceof ServerError ? err.code : ServerErrorCode.UnknownError;
      return {
        response: {
          message_id: messageId ?? "",
          error_code: errorCode,
          details: err.message
        }
      };
    }
  }
  async #getServerInfo() {
    await this.#commandHandler.start();
    const {
      fabricId: fabric_id,
      compressedFabricId: compressed_fabric_id,
      fabricIndex: fabric_index
    } = await this.#commandHandler.getCommissionerFabricData();
    return {
      fabric_id,
      compressed_fabric_id,
      fabric_index,
      schema_version: SCHEMA_VERSION,
      min_supported_schema_version: SCHEMA_VERSION,
      sdk_version: `matter-server/${this.#serverVersion} (matter.js/${MATTER_VERSION})`,
      wifi_credentials_set: !!(this.#config.wifiSsid && this.#config.wifiCredentials),
      wifi_ssid: this.#config.wifiSsid && this.#config.wifiCredentials ? this.#config.wifiSsid : void 0,
      thread_credentials_set: !!this.#config.threadDataset,
      bluetooth_enabled: this.#commandHandler.bleEnabled,
      ble_proxy_enabled: this.#commandHandler.bleProxyEnabled,
      controller_node_id: this.#commandHandler.getCommissionerNodeId()
    };
  }
  /**
   * Broadcast an event to all connected WebSocket clients.
   */
  #broadcastEvent(event, data) {
    if (!this.#wss || this.#closed || this.#shuttingDown) return;
    const message = toBigIntAwareJson({ event, data });
    this.#wss.clients.forEach((client) => {
      if (client.readyState === 1) {
        try {
          client.send(message);
        } catch (err) {
          logger.warn(`Failed to broadcast ${event} event to client`, err);
        }
      }
    });
  }
  /**
   * Send server_info_updated event to all connected clients.
   */
  async #broadcastServerInfoUpdated() {
    const serverInfo = await this.#getServerInfo();
    logger.info("Broadcasting server_info_updated event", serverInfo);
    this.#broadcastEvent("server_info_updated", serverInfo);
  }
  #handleStartListening(_args) {
    logger.info("WebSocket server start_listening");
    const data = this.#handleGetNodes({});
    logger.info("WebSocket server start_listening. Returned", data.length, "nodes");
    return data;
  }
  async #handleSetDefaultFabricLabel(args) {
    const { label } = args;
    let effectiveLabel = label && label.trim() !== "" ? label.trim() : "HomeAssistant";
    effectiveLabel = effectiveLabel.substring(0, 32);
    await this.#config.set({ fabricLabel: effectiveLabel });
    await this.#commandHandler.setFabricLabel(effectiveLabel);
    return null;
  }
  /**
   * Commission a node, allocating a fresh node id for each attempt.
   *
   * The node id counter can drift out of sync with the fabric (e.g. after manual storage edits or legacy imports).
   * When that happens matter.js rejects the chosen id with an identity conflict; we then allocate the next id and
   * retry rather than failing the commissioning outright.
   */
  async #commissionWithRetry(buildRequest) {
    let lastError;
    for (let attempt = 1; attempt <= MAX_COMMISSION_NODE_ID_ATTEMPTS; attempt++) {
      const nodeId = NodeId(
        await this.#config.allocateNodeId((id) => this.#commandHandler.isNodeIdInUse(NodeId(id)))
      );
      try {
        const { nodeId: committed } = await this.#commandHandler.commissionNode(buildRequest(nodeId));
        return committed;
      } catch (error) {
        if (!isIdentityConflict(error)) {
          throw error;
        }
        lastError = error;
        logger.warn(
          `Node id ${nodeId} conflicts with an existing node on the fabric (attempt ${attempt}/${MAX_COMMISSION_NODE_ID_ATTEMPTS}), retrying with the next id`
        );
      }
    }
    const reason = lastError instanceof Error ? `: ${lastError.message}` : "";
    throw ServerError.nodeCommissionFailed(
      `Commission failed: could not find a free node id after ${MAX_COMMISSION_NODE_ID_ATTEMPTS} attempts${reason}`,
      lastError instanceof Error ? lastError : void 0
    );
  }
  async #handleCommissionWithCode(args) {
    const { code, network_only } = args;
    const isQrCode = code.startsWith("MT:");
    let wifiCredentials = void 0;
    let threadCredentials = void 0;
    if (!network_only && this.#commandHandler.bleEnabled) {
      if (this.#config.wifiSsid && this.#config.wifiCredentials) {
        wifiCredentials = {
          wifiSsid: this.#config.wifiSsid,
          wifiCredentials: this.#config.wifiCredentials
        };
      }
      if (this.#config.threadDataset) {
        threadCredentials = {
          networkName: "",
          // Thread network name is not needed when providing operational dataset
          operationalDataset: this.#config.threadDataset
        };
      }
    }
    await this.#controller.certificateService();
    const nodeId = await this.#commissionWithRetry((nodeId2) => ({
      nodeId: nodeId2,
      onNetworkOnly: network_only,
      ...isQrCode ? { qrCode: code } : { manualCode: code },
      wifiCredentials,
      threadCredentials
    }));
    return this.#collectNodeDetails(nodeId);
  }
  async #handleCommissionOnNetwork(args) {
    const { setup_pin_code, filter_type, filter, ip_addr } = args;
    let commissionRequest;
    const baseRequest = {
      onNetworkOnly: true,
      // commission_on_network is always network-only
      // Ignore fe80 addresses and better do generic discovery because could be from mobile devices
      knownAddress: ip_addr && !ip_addr.startsWith("fe80") ? { ip: ip_addr, port: 5540 } : void 0
    };
    switch (filter_type) {
      case 1:
        if (filter === void 0)
          throw ServerError.invalidArguments("filter required for filter_type 1 (short discriminator)");
        commissionRequest = { ...baseRequest, passcode: setup_pin_code, shortDiscriminator: filter };
        break;
      case 2:
        if (filter === void 0)
          throw ServerError.invalidArguments("filter required for filter_type 2 (long discriminator)");
        commissionRequest = { ...baseRequest, passcode: setup_pin_code, longDiscriminator: filter };
        break;
      case 3:
        if (filter === void 0)
          throw ServerError.invalidArguments("filter required for filter_type 3 (vendor ID)");
        commissionRequest = { ...baseRequest, passcode: setup_pin_code, vendorId: filter, productId: 0 };
        break;
      case 4:
      // Device type - not directly supported, fall back to no filter
      case 0:
      // No filter
      default:
        commissionRequest = { ...baseRequest, passcode: setup_pin_code };
        break;
    }
    await this.#controller.certificateService();
    const nodeId = await this.#commissionWithRetry((nodeId2) => ({ ...commissionRequest, nodeId: nodeId2 }));
    return this.#collectNodeDetails(nodeId);
  }
  #handleGetNodes(args) {
    const { only_available = false } = args ?? {};
    const nodeDetails = new Array();
    for (const node of this.#commandHandler.getNodeIds()) {
      const details = this.#collectNodeDetails(node);
      if (!only_available || details.available) {
        nodeDetails.push(details);
      }
    }
    for (const testNode of this.#testNodeHandler.getNodes()) {
      if (!only_available || testNode.available) {
        nodeDetails.push(testNode);
      }
    }
    return nodeDetails;
  }
  async #handleGetNode(args) {
    const { node_id } = args;
    const nodeId = NodeId(node_id);
    const handler = this.#handlerFor(node_id);
    if (!handler.hasNode(nodeId)) {
      throw ServerError.nodeNotExists(node_id);
    }
    if (handler === this.#commandHandler) {
      await this.#commandHandler.ensureNodePopulated(nodeId);
      return this.#commandHandler.getNodeDetails(nodeId, this.#lastInterviewDates.get(nodeId));
    }
    return handler.getNodeDetails(nodeId);
  }
  async #handleGetNodeIpAddresses(args) {
    const { node_id, prefer_cache, scoped } = args;
    const result = await this.#handlerFor(node_id).getNodeIpAddresses(NodeId(node_id), prefer_cache);
    if (scoped) {
      return result;
    }
    return result.map((ip) => ip.includes("%") ? ip.split("%")[0] : ip);
  }
  async #handleReadAttribute(args) {
    const { node_id: nodeId, attribute_path, fabric_filtered = false } = args;
    const attributePaths = Array.isArray(attribute_path) ? attribute_path : [attribute_path];
    const result = await this.#handlerFor(nodeId).handleReadAttributes(
      NodeId(nodeId),
      attributePaths,
      fabric_filtered
    );
    if (Object.keys(result).length === 0) {
      throw ServerError.sdkStackError("Failed to read attribute: no values returned");
    }
    return result;
  }
  async #handleWriteAttribute(args) {
    const { node_id: nodeId, attribute_path, value } = args;
    const { endpointId, clusterId, attributeId } = splitAttributePath(attribute_path);
    if (endpointId === void 0 || clusterId === void 0 || attributeId === void 0) {
      throw ServerError.invalidArguments("write_attribute does not support wildcards in attribute path");
    }
    const { status } = await this.#handlerFor(nodeId).handleWriteAttribute({
      nodeId: NodeId(nodeId),
      endpointId,
      clusterId,
      attributeId,
      value
    });
    return [
      {
        Path: { EndpointId: endpointId, ClusterId: clusterId, AttributeId: attributeId },
        Status: status ?? 0
      }
    ];
  }
  async #handleGetVendorNames(args) {
    const { filter_vendors } = args;
    const dclVendors = await this.#controller.getAllVendors();
    const mergedVendors = {};
    for (const [vendorIdStr, vendorName] of Object.entries(VendorIds)) {
      mergedVendors[vendorIdStr] = vendorName;
    }
    for (const [vendorId, vendorInfo] of dclVendors) {
      mergedVendors[vendorId] = vendorInfo.vendorName;
    }
    if (!filter_vendors || !filter_vendors.length) {
      return mergedVendors;
    }
    const result = {};
    for (const vendorId of filter_vendors) {
      const vendorName = mergedVendors[vendorId];
      if (vendorName) {
        result[vendorId] = vendorName;
      }
    }
    return result;
  }
  async #handleDeviceCommand(args) {
    const {
      node_id: nodeId,
      endpoint_id: endpointId,
      cluster_id: clusterId,
      command_name: commandName,
      payload,
      timed_request_timeout_ms: timedInteractionTimeoutMs
    } = args;
    const result = await this.#handlerFor(nodeId).handleInvoke({
      nodeId: NodeId(nodeId),
      endpointId: EndpointNumber(endpointId),
      clusterId: ClusterId(clusterId),
      commandName: camelize(commandName),
      data: payload,
      timedInteractionTimeoutMs: typeof timedInteractionTimeoutMs === "number" ? Millis(timedInteractionTimeoutMs) : void 0
    });
    if (TestNodeCommandHandler.isTestNodeId(nodeId)) {
      return null;
    }
    const cmdResult = this.#convertCommandDataToWebSocket(ClusterId(clusterId), commandName, result);
    if (cmdResult === void 0) {
      return null;
    }
    return cmdResult;
  }
  async #handleSendWebRtcProviderCommand(args) {
    const { node_id, endpoint_id, command_name, payload } = args;
    return this.#commandHandler.sendWebRtcProviderCommand({
      nodeId: NodeId(node_id),
      endpointId: EndpointNumber(endpoint_id),
      commandName: command_name,
      payload
    });
  }
  async #handleInterviewNode(args) {
    const { node_id } = args;
    const nodeId = NodeId(node_id);
    if (TestNodeCommandHandler.isTestNodeId(nodeId)) {
      const testNode = this.#testNodeHandler.getNode(nodeId);
      if (testNode === void 0) {
        throw ServerError.nodeNotExists(nodeId);
      }
      logger.debug(`interview_node called for test node ${formatNodeId(nodeId)}`);
      testNode.last_interview = (/* @__PURE__ */ new Date()).toISOString().replace("Z", "000");
      this.#broadcastEvent("node_updated", testNode);
      return null;
    }
    await this.#commandHandler.interviewNode(nodeId);
    this.#lastInterviewDates.set(nodeId, /* @__PURE__ */ new Date());
    this.#broadcastEvent("node_updated", this.#collectNodeDetails(nodeId));
    return null;
  }
  async #handlePingNode(args) {
    const { node_id, attempts = 1 } = args;
    return await this.#handlerFor(node_id).pingNode(NodeId(node_id), attempts);
  }
  async #handleRemoveNode(args) {
    const { node_id } = args;
    await this.#handlerFor(node_id).removeNode(NodeId(node_id));
    return null;
  }
  async #handleSetWifiCredentials(args) {
    const { ssid, credentials } = args;
    await this.#config.set({ wifiSsid: ssid, wifiCredentials: credentials });
    try {
      await this.#broadcastServerInfoUpdated();
    } catch (error) {
      logger.warn("Failed to broadcast server info update", error);
    }
    return {};
  }
  async #handleSetThreadDataset(args) {
    const { dataset } = args;
    if (!/^[0-9a-fA-F]*$/.test(dataset) || dataset.length % 2 !== 0) {
      throw ServerError.invalidArguments(
        "Invalid Thread operational dataset: must be a hex string with even length (each byte is two hex characters)"
      );
    }
    try {
      Bytes.fromHex(dataset);
    } catch (error) {
      MatterError.accept(error);
      throw ServerError.invalidArguments(
        `Invalid Thread operational dataset: failed to parse hex string: ${Diagnostic.errorMessage(error)}`
      );
    }
    await this.#config.set({ threadDataset: dataset });
    try {
      await this.#broadcastServerInfoUpdated();
    } catch (error) {
      logger.warn("Failed to broadcast server info update", error);
    }
    return {};
  }
  async #handleRemoveWifiCredentials() {
    await this.#config.removeWifiCredentials();
    try {
      await this.#broadcastServerInfoUpdated();
    } catch (error) {
      logger.warn("Failed to broadcast server info update", error);
    }
    return {};
  }
  async #handleRemoveThreadDataset() {
    await this.#config.removeThreadDataset();
    try {
      await this.#broadcastServerInfoUpdated();
    } catch (error) {
      logger.warn("Failed to broadcast server info update", error);
    }
    return {};
  }
  async #handleOpenCommissioningWindow(args) {
    const {
      node_id,
      timeout
      /*, iteration, option, discriminator*/
    } = args;
    const nodeId = NodeId(node_id);
    const { manualCode, qrCode } = await this.#commandHandler.openCommissioningWindow({
      nodeId,
      timeout
    });
    const pairingCodeCodec = QrPairingCodeCodec.decode(qrCode);
    return { setup_pin_code: pairingCodeCodec[0].passcode, setup_manual_code: manualCode, setup_qr_code: qrCode };
  }
  async #handleDiscoverCommissionableNodes(_args) {
    const result = await this.#commandHandler.handleDiscovery({});
    return result.map(
      ({
        commissioningMode,
        deviceName,
        deviceType,
        hostName,
        instanceName,
        longDiscriminator,
        // numIPs,
        pairingHint,
        pairingInstruction,
        port,
        productId,
        rotatingId,
        // rotatingIdLen,
        // shortDiscriminator,
        // supportsTcpClient,
        supportsTcpServer,
        vendorId,
        addresses,
        mrpSessionActiveInterval,
        mrpSessionIdleInterval
      }) => ({
        instance_name: instanceName,
        host_name: hostName,
        // TODO
        port,
        long_discriminator: longDiscriminator,
        vendor_id: vendorId,
        product_id: productId,
        commissioning_mode: commissioningMode,
        device_type: deviceType,
        device_name: deviceName,
        pairing_instruction: pairingInstruction,
        pairing_hint: pairingHint,
        mrp_retry_interval_idle: mrpSessionIdleInterval,
        mrp_retry_interval_active: mrpSessionActiveInterval,
        supports_tcp: supportsTcpServer,
        addresses,
        rotating_id: rotatingId
      })
    );
  }
  async #handleGetMatterFabrics(args) {
    const { node_id } = args;
    const nodeId = NodeId(node_id);
    const fabrics = await this.#commandHandler.getFabrics(nodeId);
    return fabrics.map(({ fabricId, vendorId, fabricIndex, label }) => ({
      fabric_id: fabricId,
      vendor_id: vendorId,
      fabric_index: fabricIndex,
      fabric_label: label,
      vendor_name: VendorIds[vendorId]
    }));
  }
  async #handleRemoveMatterFabric(args) {
    const { node_id, fabric_index } = args;
    await this.#commandHandler.removeFabric(NodeId(node_id), FabricIndex(fabric_index));
    return {};
  }
  async #handleSetAclEntry(args) {
    const { node_id, entry } = args;
    return await this.#commandHandler.setAclEntry(NodeId(node_id), entry);
  }
  async #handleSetNodeBinding(args) {
    const { node_id, endpoint, bindings } = args;
    return await this.#commandHandler.setNodeBinding(NodeId(node_id), EndpointNumber(endpoint), bindings);
  }
  async #handleImportTestNode(args) {
    const { dump } = args;
    this.#testNodeHandler.importTestNodes(dump);
    return null;
  }
  async #handleCheckNodeUpdate(args) {
    const { node_id } = args;
    return await this.#commandHandler.checkNodeUpdate(NodeId(node_id));
  }
  async #handleUpdateNode(args) {
    const { node_id, software_version } = args;
    const targetVersion = typeof software_version === "string" ? parseInt(software_version, 10) : software_version;
    return await this.#commandHandler.updateNode(NodeId(node_id), targetVersion);
  }
  #collectNodeDetails(nodeId) {
    const lastInterviewDate = this.#lastInterviewDates.get(nodeId);
    return this.#commandHandler.getNodeDetails(nodeId, lastInterviewDate);
  }
  #convertCommandDataToWebSocket(clusterId, commandName, value, clusterData) {
    if (!clusterData) {
      clusterData = ClusterMap[clusterId];
    }
    if (clusterData === void 0 || clusterData.commands[commandName.toLowerCase()] === void 0) {
      logger.warn(
        `Cluster ${clusterId} does not have command ${commandName}. Do not convert data to WebSocket format`,
        value
      );
      return {};
    }
    return convertMatterToWebSocketNameBased(
      value,
      clusterData.commands[commandName.toLowerCase()].responseModel,
      clusterData.model
    );
  }
  /**
   * Map internal LogLevel enum to API string format.
   */
  #logLevelToString(level) {
    switch (level) {
      case LogLevel.FATAL:
        return "critical";
      case LogLevel.ERROR:
        return "error";
      case LogLevel.WARN:
        return "warning";
      case LogLevel.NOTICE:
        return "notice";
      case LogLevel.INFO:
        return "info";
      case LogLevel.DEBUG:
        return "debug";
      default:
        return "info";
    }
  }
  /**
   * Map API string format to internal LogLevel enum.
   */
  #stringToLogLevel(level) {
    switch (level) {
      case "critical":
      case "fatal":
        return LogLevel.FATAL;
      case "error":
        return LogLevel.ERROR;
      case "warning":
      case "warn":
        return LogLevel.WARN;
      case "notice":
        return LogLevel.NOTICE;
      case "info":
        return LogLevel.INFO;
      case "debug":
        return LogLevel.DEBUG;
      default:
        return LogLevel.INFO;
    }
  }
  #handleGetLogLevel() {
    const currentLevel = typeof Logger.level === "string" ? this.#stringToLogLevel(Logger.level) : Logger.level;
    const consoleLevel = this.#logLevelToString(currentLevel);
    let fileLevel = null;
    try {
      const fileDestination = Logger.destinations.file;
      const fileLevelValue = typeof fileDestination.level === "string" ? this.#stringToLogLevel(fileDestination.level) : fileDestination.level;
      fileLevel = this.#logLevelToString(fileLevelValue);
    } catch {
    }
    return {
      console_loglevel: consoleLevel,
      file_loglevel: fileLevel
    };
  }
  #handleSetLogLevel(args) {
    const { console_loglevel, file_loglevel } = args;
    if (console_loglevel !== void 0) {
      Logger.level = this.#stringToLogLevel(console_loglevel);
      logger.info(`Console log level set to: ${console_loglevel}`);
    }
    if (file_loglevel !== void 0) {
      try {
        const fileDestination = Logger.destinations.file;
        fileDestination.level = this.#stringToLogLevel(file_loglevel);
        logger.info(`File log level set to: ${file_loglevel}`);
      } catch {
      }
    }
    return this.#handleGetLogLevel();
  }
}
export {
  WebSocketControllerHandler
};
//# sourceMappingURL=WebSocketControllerHandler.js.map
