/**
 * @license
 * Copyright 2025-2026 Open Home Foundation
 * SPDX-License-Identifier: Apache-2.0
 */
import { ClusterBehavior, Diagnostic, Logger, MatterError, Millis, Time } from "@matter/main";
import { ClusterMap } from "../model/ModelMapper.js";
import { buildAttributePath, convertMatterToWebSocketTagBased } from "../server/Converters.js";
import { formatNodeId } from "../util/formatNodeId.js";
const logger = Logger.get("AttributeDataCache");
class AttributeDataCache {
  #cache = /* @__PURE__ */ new Map();
  #inFlight = /* @__PURE__ */ new Map();
  /**
   * Add a node to the cache and populate its attributes.
   * No entry is created if the node is not yet initialized.
   */
  add(node) {
    return this.#populateFromNode(node, false);
  }
  /**
   * Remove a node from the cache.
   */
  delete(nodeId) {
    this.#cache.delete(nodeId);
    const context = this.#inFlight.get(nodeId);
    if (context !== void 0) {
      context.cancelled = true;
      this.#inFlight.delete(nodeId);
    }
  }
  /**
   * Update (reinitialize) the cache for a node.
   * Creates a fresh cache from the node's current state.
   * Use this when the node structure may have changed (endpoints added/removed).
   */
  update(node) {
    return this.#populateFromNode(node, true);
  }
  /**
   * Update a single attribute in the cache.
   * Use this for incremental updates when an attribute value changes.
   */
  updateAttribute(nodeId, data) {
    const { endpointId, clusterId, attributeId } = data.path;
    const clusterData = ClusterMap[clusterId];
    const convertedValue = convertMatterToWebSocketTagBased(
      data.value,
      clusterData?.attributes[attributeId],
      clusterData?.model
    );
    if (convertedValue === void 0) {
      return;
    }
    const path = buildAttributePath(endpointId, clusterId, attributeId);
    const inFlight = this.#inFlight.get(nodeId);
    const attributes = this.#cache.get(nodeId);
    if (attributes !== void 0) {
      attributes[path] = convertedValue;
    }
    inFlight?.pending.push([path, convertedValue]);
  }
  /**
   * Get cached attributes for a node.
   * Returns undefined if no cache exists for the node.
   */
  get(nodeId) {
    return this.#cache.get(nodeId);
  }
  /**
   * Check if a node exists in the cache.
   */
  has(nodeId) {
    return this.#cache.has(nodeId);
  }
  /**
   * Populate the cache for a node from its current state.
   * Creates a completely fresh flat attribute object.
   *
   * Collecting attributes for a large node (~90 endpoints) is heavy synchronous work, so it is
   * chunked with event-loop yields. Concurrent calls for the same node collapse onto the running
   * populate instead of building competing snapshots.
   *
   * `rebuild` distinguishes a data-changed caller (state/structure change) that needs the running
   * pass redone from a caller that merely awaits completion (a read). Only the former schedules a
   * re-run; reads just await the in-flight promise, so frequent reads can never thrash the populate.
   */
  #populateFromNode(node, rebuild) {
    const nodeId = node.nodeId;
    if (!node.initialized || !node.node.lifecycle.isCommissioned || !node.node.lifecycle.isReady) {
      logger.debug(`Node ${formatNodeId(nodeId)} not initialized, skipping cache population`);
      return Promise.resolve();
    }
    const inFlight = this.#inFlight.get(nodeId);
    if (inFlight !== void 0) {
      if (rebuild) {
        inFlight.rerun = true;
        logger.debug(`Populate for node ${formatNodeId(nodeId)} already running, scheduling re-run`);
      }
      return inFlight.promise;
    }
    const context = { rerun: false, cancelled: false, pending: [], promise: Promise.resolve() };
    context.promise = this.#runPopulate(node, context);
    this.#inFlight.set(nodeId, context);
    return context.promise;
  }
  async #runPopulate(node, context) {
    const nodeId = node.nodeId;
    try {
      let attributeCount = 0;
      const startedAt = Time.nowMs;
      do {
        context.rerun = false;
        context.pending = [];
        const attributes = {};
        await this.#collectAttributes(node.node, attributes, context);
        if (this.#inFlight.get(nodeId) !== context) {
          return;
        }
        if (context.rerun) {
          continue;
        }
        for (const [path, value] of context.pending) {
          attributes[path] = value;
        }
        this.#cache.set(nodeId, attributes);
        attributeCount = Object.keys(attributes).length;
      } while (context.rerun);
      logger.debug(
        `Populated attribute cache for node ${formatNodeId(nodeId)}: ${attributeCount} attributes in ${Time.nowMs - startedAt}ms`
      );
    } catch (error) {
      logger.warn(`Failed to populate attribute cache for node ${formatNodeId(nodeId)}:`, error);
    } finally {
      if (this.#inFlight.get(nodeId) === context) {
        this.#inFlight.delete(nodeId);
      }
    }
  }
  /**
   * Collect attributes from all endpoints into a flat attribute object, yielding to the event loop
   * between endpoints so a large node does not block other timers, I/O, and WebSocket traffic.
   */
  async #collectAttributes(node, attributes, context) {
    for (const endpoint of node.endpoints) {
      const endpointId = endpoint.number;
      for (const behavior of endpoint.behaviors.active) {
        if (!ClusterBehavior.is(behavior)) {
          continue;
        }
        const cluster = behavior.cluster;
        const clusterData = ClusterMap[cluster.id];
        const clusterState = endpoint.stateOf(behavior);
        for (const attribute of cluster.schema.attributes) {
          try {
            const convertedValue = convertMatterToWebSocketTagBased(
              clusterState[attribute.propertyName],
              clusterData?.attributes[attribute.id],
              clusterData?.model
            );
            if (convertedValue === void 0) {
              continue;
            }
            attributes[buildAttributePath(endpointId, cluster.id, attribute.id)] = convertedValue;
          } catch (error) {
            MatterError.accept(error);
            logger.debug(
              `Ignoring Attribute ${attribute.propertyName} because of`,
              Diagnostic.errorMessage(error)
            );
          }
        }
      }
      await Time.sleep("AttributeDataCache populate yield", Millis(0));
      if (context.cancelled || context.rerun) {
        return;
      }
    }
  }
}
export {
  AttributeDataCache
};
//# sourceMappingURL=AttributeDataCache.js.map
