/**
 * @license
 * Copyright 2025-2026 Open Home Foundation
 * SPDX-License-Identifier: Apache-2.0
 */
import { Logger, Observable } from "@matter/main";
import { WebSocket, WebSocketServer } from "ws";
import { BleProxyConnection } from "./BleProxyConnection.js";
import { BleProxyCommand, BleProxyEvent } from "./BleProxyProtocol.js";
const logger = Logger.get("BleProxyHandler");
class BleProxyHandler {
  #wss;
  /** Upgrade listener removers, one per HTTP server `register()` call (multi-bind). */
  #removeUpgradeListeners = [];
  #connections = /* @__PURE__ */ new Set();
  #closed = false;
  /** Active scan intent + args, so clients joining mid-scan can be synced. */
  #scanActive = false;
  #scanArgs;
  /** Connections currently told to scan, for aggregate `scanStopped`. */
  #scanning = /* @__PURE__ */ new Set();
  /** address -> owning connection + every connection that has seen it. */
  #owners = /* @__PURE__ */ new Map();
  /** Emitted whenever a connection completes its handshake. */
  connectionEstablished = new Observable();
  /** Emitted for each `device_discovered`, after ownership is updated. */
  deviceDiscovered = new Observable();
  /** Emitted once no connection is scanning anymore. */
  scanStopped = new Observable();
  get connected() {
    for (const c of this.#connections) {
      if (c.connected) return true;
    }
    return false;
  }
  async register(server) {
    const isFirstBind = this.#wss === void 0;
    const wss = this.#wss ??= new WebSocketServer({ noServer: true });
    const upgradeHandler = (req, socket, head) => {
      logger.debug(`Upgrade request received: ${req.url}`);
      const path = req.url?.split("?")[0];
      if (path === "/ble") {
        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));
    logger.info("BLE proxy WebSocket endpoint registered on /ble");
    if (!isFirstBind) {
      return;
    }
    wss.on("connection", (ws) => {
      if (this.#closed) {
        ws.close();
        return;
      }
      const connection = new BleProxyConnection(ws);
      this.#connections.add(connection);
      connection.handshakeCompleted.on(() => this.#onHandshakeCompleted(connection));
      connection.eventReceived.on((event, data) => this.#onConnectionEvent(connection, event, data));
      connection.closed.on(() => this.#onConnectionClosed(connection));
    });
  }
  unregister() {
    if (!this.#wss || this.#closed) {
      return Promise.resolve();
    }
    this.#closed = true;
    for (const remove of this.#removeUpgradeListeners) remove();
    this.#removeUpgradeListeners = [];
    for (const connection of this.#connections) {
      connection.close();
    }
    this.#connections.clear();
    this.#scanning.clear();
    this.#owners.clear();
    const wss = this.#wss;
    this.#wss = void 0;
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.close();
      }
    });
    return new Promise((resolve, reject) => {
      wss.close((err) => err ? reject(err) : resolve());
    });
  }
  /** Returns the connection that owns `address`, if it is still connected. */
  getOwner(address) {
    const entry = this.#owners.get(address);
    if (entry && entry.owner.connected) return entry.owner;
    return void 0;
  }
  async startScan(args) {
    this.#scanActive = true;
    this.#scanArgs = args;
    const sends = new Array();
    for (const connection of this.#connections) {
      if (connection.connected) {
        this.#scanning.add(connection);
        sends.push(
          connection.sendCommand(BleProxyCommand.StartScan, args).catch((err) => {
            logger.warn(`[${connection.id}] Failed to start scan:`, err);
            this.#markNotScanning(connection, "start scan failed");
          })
        );
      }
    }
    await Promise.all(sends);
  }
  async stopScan() {
    this.#scanActive = false;
    const sends = new Array();
    for (const connection of this.#connections) {
      if (connection.connected) {
        sends.push(
          connection.sendCommand(BleProxyCommand.StopScan).catch((err) => logger.warn(`[${connection.id}] Failed to stop scan:`, err))
        );
      }
    }
    this.#scanning.clear();
    await Promise.all(sends);
  }
  #onHandshakeCompleted(connection) {
    this.connectionEstablished.emit();
    if (this.#scanActive && this.#scanArgs) {
      this.#scanning.add(connection);
      connection.sendCommand(BleProxyCommand.StartScan, this.#scanArgs).catch((err) => {
        logger.warn(`[${connection.id}] Failed to sync scan to joining client:`, err);
        this.#markNotScanning(connection, "start scan failed");
      });
    }
  }
  #onConnectionEvent(connection, event, data) {
    if (event === BleProxyEvent.DeviceDiscovered) {
      this.#onDeviceDiscovered(connection, data);
    } else if (event === BleProxyEvent.ScanStopped) {
      this.#markNotScanning(connection, data.reason ?? "unknown");
    }
  }
  /** Drop a connection from the scanning set; emit aggregate scanStopped once none remain. */
  #markNotScanning(connection, reason) {
    if (!this.#scanning.delete(connection)) {
      return;
    }
    if (this.#scanning.size === 0) {
      this.scanStopped.emit(reason);
    }
  }
  #onDeviceDiscovered(connection, data) {
    let entry = this.#owners.get(data.address);
    if (!entry) {
      entry = { owner: connection, seers: /* @__PURE__ */ new Set([connection]) };
      this.#owners.set(data.address, entry);
    } else {
      entry.seers.add(connection);
      if (!entry.owner.connected) {
        entry.owner = connection;
      }
    }
    logger.debug(
      `[${connection.id}] device_discovered ${data.address} rssi=${data.rssi ?? "n/a"} seers=${entry.seers.size} isOwner=${entry.owner === connection}`
    );
    this.deviceDiscovered.emit(data, connection);
  }
  #onConnectionClosed(connection) {
    this.#connections.delete(connection);
    this.#markNotScanning(connection, "client disconnected");
    for (const [address, entry] of this.#owners) {
      entry.seers.delete(connection);
      if (entry.owner === connection) {
        let next;
        for (const seer of entry.seers) {
          if (seer.connected) {
            next = seer;
            break;
          }
        }
        if (next) {
          entry.owner = next;
          logger.info(`Reassigned ownership of ${address} to [${next.id}]`);
        } else {
          this.#owners.delete(address);
        }
      }
    }
  }
}
export {
  BleProxyHandler
};
//# sourceMappingURL=BleProxyHandler.js.map
