/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { ClusterBehavior } from "#behavior/cluster/ClusterBehavior.js";
import { isClientBehavior } from "#behavior/cluster/cluster-behavior-utils.js";
import { ClientGroup } from "#node/ClientGroup.js";
import { Node } from "#node/Node.js";
import { ServerNode } from "#node/ServerNode.js";
import { BasicMultiplex, Diagnostic, Environmental, InternalError, Logger } from "@matter/general";
import { FabricManager, PeerAddress, PeerSet } from "@matter/protocol";
import { NodeId } from "@matter/types";
import { BindingServer } from "./BindingServer.js";
const logger = Logger.get("BindingManager");
class BindingManager {
  #env;
  #cachedNode;
  #cachedFabrics;
  #queue = new Array();
  /**
   * Keyed by the source endpoint (stable object identity across behavior proxy contexts).
   * Stores both pending and established state plus the canonical server reference for event emission.
   */
  #serverMap = /* @__PURE__ */ new Map();
  #refcounts = /* @__PURE__ */ new Map();
  #multiplex = new BasicMultiplex();
  #flushed = false;
  constructor(env) {
    this.#env = env;
    const node = env.get(Node);
    if (node.lifecycle.isOnline) {
      this.#captureRefs(node);
      this.#flushed = true;
    }
    node.lifecycle.online.on(() => this.#onOnline(node));
  }
  #onOnline(node) {
    this.#captureRefs(node);
    this.#multiplex.add(this.#flushQueue(), "binding queue flush");
  }
  #captureRefs(node) {
    if (!(node instanceof ServerNode)) {
      throw new InternalError("BindingManager requires a ServerNode environment");
    }
    this.#cachedNode = node;
    this.#cachedFabrics = this.#env.get(FabricManager);
  }
  get #node() {
    if (this.#cachedNode === void 0) {
      throw new InternalError("BindingManager.node accessed before node online");
    }
    return this.#cachedNode;
  }
  get #fabrics() {
    if (this.#cachedFabrics === void 0) {
      throw new InternalError("BindingManager.fabrics accessed before node online");
    }
    return this.#cachedFabrics;
  }
  #record(server) {
    const ep = server.endpoint;
    let rec = this.#serverMap.get(ep);
    if (rec === void 0) {
      rec = { server, pending: /* @__PURE__ */ new Map(), established: /* @__PURE__ */ new Map() };
      this.#serverMap.set(ep, rec);
    }
    return rec;
  }
  register(server, sourceEndpoint, entry) {
    if (!this.#flushed) {
      this.#queue.push({ server, endpoint: sourceEndpoint, entry });
      return;
    }
    this.#multiplex.add(this.#resolveAndEmit({ server, endpoint: sourceEndpoint, entry }), "binding resolve");
  }
  async unregister(server, entry) {
    if (this.#clearPending(server, entry)) {
      return;
    }
    const rec = this.#serverMap.get(server.endpoint);
    if (rec === void 0) {
      return;
    }
    const key = BindingManager.entryKey(entry);
    const established = rec.established.get(key);
    if (established === void 0) {
      return;
    }
    rec.established.delete(key);
    if (rec.established.size === 0 && rec.pending.size === 0) {
      this.#serverMap.delete(server.endpoint);
    }
    const { resolution, ref } = established;
    if (ref !== void 0) {
      const count = (this.#refcounts.get(ref) ?? 0) - 1;
      if (count <= 0) {
        this.#refcounts.delete(ref);
      } else {
        this.#refcounts.set(ref, count);
      }
    }
    logger.debug(
      "Binding removed",
      Diagnostic.dict({
        endpoint: rec.server.endpoint.number,
        kind: resolution.kind,
        entry: resolution.entry
      })
    );
    try {
      await rec.server.events.removed.emit(resolution);
    } catch (err) {
      logger.warn(
        "Binding removed handler failed",
        Diagnostic.dict({ endpoint: rec.server.endpoint.number, kind: resolution.kind }),
        Diagnostic.error(err)
      );
    }
  }
  async disposeServer(server) {
    const rec = this.#serverMap.get(server.endpoint);
    if (rec === void 0) return;
    const snapshot = [...rec.established.values()];
    this.#serverMap.delete(server.endpoint);
    for (const { ref } of snapshot) {
      if (ref !== void 0) {
        const count = (this.#refcounts.get(ref) ?? 0) - 1;
        if (count <= 0) this.#refcounts.delete(ref);
        else this.#refcounts.set(ref, count);
      }
    }
    for (const { resolution } of snapshot) {
      logger.debug(
        "Binding removed",
        Diagnostic.dict({
          endpoint: rec.server.endpoint.number,
          kind: resolution.kind,
          entry: resolution.entry
        })
      );
      try {
        await rec.server.events.removed.emit(resolution);
      } catch (err) {
        logger.error(
          "Binding removed handler failed",
          Diagnostic.dict({ endpoint: rec.server.endpoint.number, kind: resolution.kind }),
          Diagnostic.error(err)
        );
      }
    }
  }
  async #flushQueue() {
    this.#flushed = true;
    const items = [...this.#queue];
    this.#queue.length = 0;
    for (const item of items) {
      await this.#resolveAndEmit(item);
    }
  }
  async #resolveAndEmit(item) {
    const { server, endpoint: sourceEp, entry } = item;
    const hasNode = entry.node !== void 0;
    const hasGroup = entry.group !== void 0;
    if (hasNode === hasGroup) {
      logger.warn("Binding entry must have exactly one of node/group", Diagnostic.dict({ entry }));
      return;
    }
    let resolution;
    const declaredClients = this.#selectClientClusters(sourceEp, entry.cluster);
    if (declaredClients === void 0) {
      logger.warn(
        "Binding source endpoint declares no matching client cluster",
        Diagnostic.dict({ entry, sourceEndpoint: sourceEp.number })
      );
      return;
    }
    if (hasGroup) {
      if (!this.#fabrics.has(entry.fabricIndex)) {
        logger.warn("Group binding fabric unknown", Diagnostic.dict({ entry }));
        return;
      }
      const fabric = this.#fabrics.for(entry.fabricIndex);
      const memberEndpoints = fabric.groups.endpoints.get(entry.group) ?? [];
      if (!memberEndpoints.includes(sourceEp.number)) {
        logger.warn(
          "Group binding source endpoint is not a member of the bound group",
          Diagnostic.dict({ entry, sourceEndpoint: sourceEp.number })
        );
        return;
      }
      const addr = PeerAddress({
        fabricIndex: entry.fabricIndex,
        nodeId: NodeId.fromGroupId(entry.group)
      });
      let group;
      try {
        group = await this.#node.peers.forAddress(addr);
      } catch (error) {
        logger.warn("Group binding peer registration failed", Diagnostic.dict({ entry }), error);
        return;
      }
      if (!(group instanceof ClientGroup)) {
        logger.warn("Group binding did not resolve to a ClientGroup", Diagnostic.dict({ entry }));
        return;
      }
      const endpoint = group.endpoints.require(sourceEp.number);
      this.#installClientBehaviors(endpoint, declaredClients);
      resolution = { kind: "group", node: group, endpoint, entry };
    } else if (this.#isOurNode(entry.node, entry.fabricIndex)) {
      if (entry.endpoint === void 0 || !this.#node.endpoints.has(entry.endpoint)) {
        logger.warn("Self-binding to non-existent endpoint", Diagnostic.dict({ endpoint: entry.endpoint }));
        return;
      }
      const endpoint = this.#node.endpoints.for(entry.endpoint);
      if (entry.cluster !== void 0 && !this.#endpointHasClusterServer(endpoint, entry.cluster)) {
        logger.warn(
          "Self-binding references cluster not installed as server on target endpoint",
          Diagnostic.dict({ entry })
        );
        return;
      }
      resolution = { kind: "server", node: this.#node, endpoint, entry };
    } else {
      if (entry.endpoint === void 0) {
        logger.warn("Client binding entry missing endpoint", Diagnostic.dict({ entry }));
        return;
      }
      const addr = PeerAddress({ fabricIndex: entry.fabricIndex, nodeId: entry.node });
      let peer;
      try {
        peer = await this.#node.peers.forAddress(addr);
      } catch (error) {
        logger.warn("Client binding peer registration failed", Diagnostic.dict({ entry }), error);
        return;
      }
      const endpoint = peer.endpoints.require(entry.endpoint);
      this.#installClientBehaviors(endpoint, declaredClients);
      resolution = { kind: "client", node: peer, endpoint, entry };
    }
    if (resolution.kind === "client") {
      this.#establishClientKind(server, resolution);
      return;
    }
    this.#recordEstablished(server, resolution);
    const { server: canonicalServer } = this.#record(server);
    if (!this.#shouldEmitEstablished(canonicalServer, resolution)) {
      return;
    }
    logger.debug(
      "Binding established",
      Diagnostic.dict({
        endpoint: canonicalServer.endpoint.number,
        kind: resolution.kind,
        entry: resolution.entry
      })
    );
    try {
      await canonicalServer.events.established.emit(resolution);
    } catch (err) {
      logger.warn(
        "Binding established handler failed",
        Diagnostic.dict({ endpoint: sourceEp.number, kind: resolution.kind }),
        Diagnostic.error(err)
      );
    }
  }
  #endpointHasClusterServer(endpoint, clusterId) {
    return Object.values(endpoint.behaviors.supported).some(
      (b) => ClusterBehavior.is(b) && !isClientBehavior(b) && b.cluster.id === clusterId
    );
  }
  #selectClientClusters(sourceEp, filterCluster) {
    const declared = sourceEp.type.clientClusters;
    if (declared === void 0) {
      return void 0;
    }
    const clients = Object.values(declared).filter((b) => ClusterBehavior.is(b));
    if (clients.length === 0) {
      return void 0;
    }
    const selected = filterCluster === void 0 ? clients : clients.filter((c) => c.cluster.id === filterCluster);
    if (selected.length === 0) {
      return void 0;
    }
    return selected;
  }
  #installClientBehaviors(endpoint, clients) {
    for (const client of clients) {
      endpoint.behaviors.require(client);
    }
  }
  #establishClientKind(server, resolution) {
    this.#multiplex.add(resolution.node.start(), `start peer ${resolution.node}`);
    try {
      const peerAddress = resolution.node.peerAddress;
      if (peerAddress !== void 0) {
        logger.info(
          "Initiating CASE session for bound peer",
          Diagnostic.dict({
            peer: this.#peerKey(peerAddress),
            sourceEndpoint: server.endpoint.number,
            entry: resolution.entry
          })
        );
        const peer = this.#node.env.get(PeerSet).for(peerAddress);
        this.#multiplex.add(peer.connect(), `CASE connect for ${this.#peerKey(peerAddress)}`);
      }
    } catch (err) {
      logger.warn("PeerSet lookup failed", Diagnostic.error(err));
    }
    const observable = resolution.node.lifecycle.online;
    const rec = this.#record(server);
    const handler = async () => {
      this.#clearPending(server, resolution.entry);
      this.#recordEstablished(server, resolution);
      if (!this.#shouldEmitEstablished(rec.server, resolution)) {
        return;
      }
      logger.debug(
        "Binding established",
        Diagnostic.dict({
          endpoint: rec.server.endpoint.number,
          kind: resolution.kind,
          entry: resolution.entry
        })
      );
      try {
        await rec.server.events.established.emit(resolution);
      } catch (err) {
        logger.error(
          "Binding established handler failed",
          Diagnostic.dict({ endpoint: server.endpoint.number, kind: resolution.kind }),
          Diagnostic.error(err)
        );
      }
    };
    if (resolution.node.lifecycle.isOnline) {
      this.#multiplex.add(handler(), "binding established (online)");
      return;
    }
    observable.once(handler);
    const cancel = () => observable.off(handler);
    const key = BindingManager.entryKey(resolution.entry);
    rec.pending.get(key)?.cancel();
    rec.pending.set(key, { cancel });
  }
  /** Returns true when emission should proceed.  Warns and returns false when no subscriber is attached. */
  #shouldEmitEstablished(server, resolution) {
    if (server.endpoint.eventsOf(BindingServer).established.isObserved) {
      return true;
    }
    if (Object.keys(server.endpoint.type.clientClusters).length > 0) {
      logger.warn(
        "Binding established on endpoint with declared client clusters but no subscriber attached",
        Diagnostic.dict({ endpoint: server.endpoint.number, kind: resolution.kind })
      );
    }
    return false;
  }
  #clearPending(server, entry) {
    const rec = this.#serverMap.get(server.endpoint);
    if (rec === void 0) {
      return false;
    }
    const key = BindingManager.entryKey(entry);
    const pending = rec.pending.get(key);
    if (pending === void 0) {
      return false;
    }
    pending.cancel();
    rec.pending.delete(key);
    if (rec.pending.size === 0 && rec.established.size === 0) {
      this.#serverMap.delete(server.endpoint);
    }
    return true;
  }
  #isOurNode(nodeId, fabricIndex) {
    if (!this.#fabrics.has(fabricIndex)) {
      return false;
    }
    return this.#fabrics.for(fabricIndex).nodeId === nodeId;
  }
  #peerKey(addr) {
    return `p:${addr.fabricIndex}:${addr.nodeId}`;
  }
  #refKey(resolution) {
    switch (resolution.kind) {
      case "client":
      case "group": {
        const addr = resolution.node.peerAddress;
        return addr !== void 0 ? this.#peerKey(addr) : void 0;
      }
      case "server":
        return void 0;
    }
  }
  #recordEstablished(server, resolution) {
    const ref = this.#refKey(resolution);
    const key = BindingManager.entryKey(resolution.entry);
    const rec = this.#record(server);
    rec.established.set(key, { resolution, ref });
    if (ref !== void 0) {
      this.#refcounts.set(ref, (this.#refcounts.get(ref) ?? 0) + 1);
    }
  }
  async close() {
    this.#flushed = true;
    this.#queue.length = 0;
    for (const rec of this.#serverMap.values()) {
      for (const { cancel } of rec.pending.values()) {
        cancel();
      }
      rec.pending.clear();
    }
    await this.#multiplex.close();
    this.#serverMap.clear();
    this.#refcounts.clear();
    this.#cachedNode = void 0;
    this.#cachedFabrics = void 0;
    this.#env.delete(BindingManager, this);
  }
  static [Environmental.create](env) {
    const instance = new BindingManager(env);
    env.set(BindingManager, instance);
    return instance;
  }
}
((BindingManager2) => {
  function entryKey(entry) {
    return [entry.fabricIndex, entry.node ?? "", entry.group ?? "", entry.endpoint ?? "", entry.cluster ?? ""].join(
      "/"
    );
  }
  BindingManager2.entryKey = entryKey;
})(BindingManager || (BindingManager = {}));
export {
  BindingManager
};
//# sourceMappingURL=BindingManager.js.map
