/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { ClusterBehavior } from "#behavior/cluster/ClusterBehavior.js";
import {
  asError,
  BasicSet,
  Diagnostic,
  Environmental,
  InternalError,
  Logger,
  ObserverGroup,
  serialize
} from "@matter/general";
import { DataModelPath } from "@matter/model";
import {
  AccessControl,
  assertRemoteActor,
  FabricManager,
  hasRemoteActor,
  PeerAddress,
  Subject
} from "@matter/protocol";
import {
  AttributeId,
  NodeId,
  Status,
  StatusResponse,
  StatusResponseError
} from "@matter/types";
import { Thermostat } from "@matter/types/clusters/thermostat";
import { AtomicWriteState } from "./AtomicWriteState.js";
const logger = Logger.get("AtomicWriteHandler");
class AtomicWriteHandler {
  #observers = new ObserverGroup();
  #pendingWrites = new BasicSet();
  constructor(fabricManager) {
    this.#observers.on(fabricManager.events.deleting, (fabric) => this.#handleFabricRemoval(fabric));
  }
  static [Environmental.create](env) {
    const instance = new AtomicWriteHandler(env.get(FabricManager));
    env.set(AtomicWriteHandler, instance);
    return instance;
  }
  close() {
    this.#observers.close();
    for (const writeState of Array.from(this.#pendingWrites)) {
      writeState.close();
    }
  }
  /**
   * Initializes an AtomicWrite state for the given request, context, endpoint and cluster.
   * It also implements all relevant validation according to the Matter spec.
   */
  #initializeState({ requestType, attributeRequests, timeout }, context, endpoint, cluster) {
    if (!ClusterBehavior.is(cluster)) {
      throw new InternalError("Cluster behavior expected for atomic write handler");
    }
    const peerAddress = this.#assertValidPeer(context);
    if (attributeRequests.length === 0) {
      throw new StatusResponse.InvalidCommandError("No attribute requests provided");
    }
    const attributes = /* @__PURE__ */ new Map();
    for (const attr of attributeRequests) {
      const [attributeName, _] = Object.entries(cluster.cluster.attributes ?? {}).find(([_2, { id }]) => id === attr) ?? [];
      if (attributeName === void 0 || endpoint.stateOf(cluster.id)[attr] === void 0) {
        throw new StatusResponse.InvalidCommandError(`Attribute ${attr} not supported by cluster`);
      }
      if (attributes.has(attr)) {
        throw new StatusResponse.InvalidCommandError("Duplicate attribute in attribute requests");
      }
      attributes.set(attr, attributeName);
    }
    const existingState = this.#pendingWriteForPeer(peerAddress, endpoint, cluster.cluster.id);
    if (requestType === Thermostat.RequestType.BeginWrite) {
      if (timeout === void 0) {
        throw new StatusResponse.InvalidCommandError("Timeout missing for BeginWrite request");
      }
      const initialValues = {};
      for (const attr of attributeRequests) {
        initialValues[attr] = endpoint.stateOf(cluster.id)[attr];
      }
      const state = new AtomicWriteState(
        peerAddress,
        endpoint,
        cluster.cluster.id,
        attributeRequests,
        timeout,
        attributes,
        initialValues
      );
      this.#pendingWrites.add(state);
      state.closed.on(() => void this.#pendingWrites.delete(state));
      state.start();
      logger.debug("Added atomic write state:", state);
      return state;
    }
    if (existingState === void 0) {
      throw new StatusResponse.InvalidInStateError("No atomic write in progress for this peer");
    }
    if (existingState.attributeRequests.length !== attributeRequests.length || !existingState.attributeRequests.every((attr) => attributeRequests.includes(attr))) {
      throw new StatusResponse.InvalidInStateError("Attribute requests do not match existing atomic write");
    }
    return existingState;
  }
  /**
   * Implements the begin write logic for an atomic write.
   */
  beginWrite(request, context, endpoint, cluster) {
    if (!hasRemoteActor(context)) {
      throw new StatusResponse.InvalidCommandError("AtomicRequest requires a remote actor");
    }
    if (!ClusterBehavior.is(cluster)) {
      throw new InternalError("Cluster behavior expected for atomic write handler");
    }
    const peerAddress = this.#derivePeerAddress(context);
    if (peerAddress !== void 0 && this.#pendingWriteForPeer(peerAddress, endpoint, cluster.cluster.id)) {
      throw new StatusResponse.InvalidInStateError(
        "An atomic write is already in progress for this peer on this cluster and endpoint"
      );
    }
    let commandStatusCode = Status.Success;
    const attributeStatus = request.attributeRequests.map((attr) => {
      let statusCode = Status.Success;
      const attributeModel = cluster.schema.conformant.attributes(attr);
      if (attributeModel === void 0) {
        statusCode = Status.InvalidCommand;
      } else {
        const { writeLevel } = cluster.supervisor.get(attributeModel).access.limits;
        const location = {
          path: DataModelPath.none,
          endpoint: endpoint.number,
          cluster: cluster.cluster.id,
          owningFabric: context.fabric
        };
        if (context.authorityAt(writeLevel, location) !== AccessControl.Authority.Granted) {
          statusCode = Status.UnsupportedAccess;
        } else if (!attributeModel.quality.atomic) {
          statusCode = Status.InvalidCommand;
        } else if (this.#pendingWriteStateForAttribute(endpoint, cluster, attr) !== void 0) {
          statusCode = Status.Busy;
        }
      }
      if (statusCode !== Status.Success) {
        commandStatusCode = Status.Failure;
      }
      return {
        attributeId: attr,
        statusCode
      };
    });
    let timeout;
    if (commandStatusCode === Status.Success) {
      const state = this.#initializeState(request, context, endpoint, cluster);
      timeout = state.timeout;
    }
    return {
      statusCode: commandStatusCode,
      attributeStatus,
      timeout
    };
  }
  /**
   * Handles writing a value for an attribute as part of an ongoing atomic write.
   * It uses the *$AtomicChanging* event to trigger validation of the partial write.
   */
  writeAttribute(context, endpoint, cluster, attribute, value) {
    const state = this.#assertPendingWriteForAttributeAndPeer(context, endpoint, cluster, attribute);
    const attributeName = state.attributeNames.get(attribute);
    logger.debug(`Writing pending value for attribute ${attributeName}, ${attribute} in atomic write`, value);
    endpoint.eventsOf(cluster.id)[`${attributeName}$AtomicChanging`]?.emit(
      value,
      state.pendingAttributeValues[attribute] !== void 0 ? state.pendingAttributeValues[attribute] : state.initialValues[attribute],
      context
    );
    state.pendingAttributeValues[attribute] = value;
    logger.debug("Atomic write state after current write:", state);
  }
  /**
   * Implements the commit logic for an atomic write.
   */
  async commitWrite(request, context, endpoint, cluster, clusterState) {
    const state = this.#initializeState(request, context, endpoint, cluster);
    let commandStatusCode = Status.Success;
    const attributeStatus = [];
    for (const [attr, value] of Object.entries(state.pendingAttributeValues)) {
      let statusCode = Status.Success;
      try {
        const attributeName = state.attributeNames.get(AttributeId(Number(attr)));
        endpoint.eventsOf(cluster.id)[`${attributeName}$AtomicChanging`]?.emit(value, endpoint.stateOf(cluster.id)[attr], context);
        endpoint.eventsOf(cluster.id)[`${attributeName}$AtomicChanged`]?.emit(value, endpoint.stateOf(cluster.id)[attr], context);
        clusterState[attr] = value;
        await context.transaction?.commit();
      } catch (error) {
        await context.transaction?.rollback();
        logger.info(
          `Failed to write attribute ${attr} during atomic write commit:`,
          Diagnostic.errorMessage(asError(error))
        );
        statusCode = StatusResponseError.of(error)?.code ?? Status.Failure;
        commandStatusCode = commandStatusCode === Status.Failure ? Status.Failure : commandStatusCode === Status.ConstraintError ? Status.ConstraintError : Status.Failure;
      }
      attributeStatus.push({
        attributeId: AttributeId(Number(attr)),
        statusCode
      });
    }
    state.close();
    return {
      statusCode: commandStatusCode,
      attributeStatus
    };
  }
  /**
   * Implements the rollback logic for an atomic write.
   */
  rollbackWrite(request, context, endpoint, cluster) {
    const state = this.#initializeState(request, context, endpoint, cluster);
    state.close();
    return {
      statusCode: Status.Success,
      attributeStatus: state.attributeRequests.map((attr) => ({
        attributeId: attr,
        statusCode: Status.Success
      }))
    };
  }
  /**
   * Handles fabric removal by closing all pending atomic write states for peers on the removed fabric.
   */
  #handleFabricRemoval(fabric) {
    const fabricIndex = fabric.fabricIndex;
    for (const writeState of Array.from(this.#pendingWrites)) {
      if (writeState.peerAddress.fabricIndex === fabricIndex) {
        logger.debug(
          `Closing atomic write state for peer ${writeState.peerAddress.toString()} on endpoint ${writeState.endpoint.id} due to fabric removal`
        );
        writeState.close();
      }
    }
  }
  /**
   * Returns the atomic write state this peer holds on the given cluster/endpoint, if any.
   */
  #pendingWriteForPeer(peerAddress, endpoint, clusterId) {
    return this.#pendingWrites.find(
      (s) => PeerAddress.is(s.peerAddress, peerAddress) && s.endpoint.number === endpoint.number && s.clusterId === clusterId
    );
  }
  /**
   * Returns the pending write state for the given attribute, if any.
   */
  #pendingWriteStateForAttribute(endpoint, cluster, attribute) {
    const writeStates = this.#pendingWrites.filter(
      (s) => s.endpoint.number === endpoint.number && s.clusterId === cluster.cluster.id
    );
    if (writeStates.length === 0) {
      return void 0;
    }
    const attrWriteStates = writeStates.filter(({ attributeRequests }) => attributeRequests.includes(attribute));
    if (attrWriteStates.length === 0) {
      return void 0;
    }
    if (attrWriteStates.length > 1) {
      throw new InternalError("Multiple atomic write states found for the same attribute. Should never happen");
    }
    return attrWriteStates[0];
  }
  /**
   * Returns the pending value for the given attribute and peer, if any.
   */
  pendingValueForAttributeAndPeer(context, endpoint, cluster, attribute) {
    const peerAddress = this.#derivePeerAddress(context);
    if (peerAddress === void 0) {
      return void 0;
    }
    const attrWriteState = this.#pendingWriteStateForAttribute(endpoint, cluster, attribute);
    if (attrWriteState === void 0) {
      return void 0;
    }
    if (!PeerAddress.is(attrWriteState.peerAddress, peerAddress)) {
      return void 0;
    }
    logger.debug(
      `Found pending value for attribute ${attribute} for peer ${peerAddress.nodeId}`,
      serialize(attrWriteState.pendingAttributeValues[attribute])
    );
    return attrWriteState.pendingAttributeValues[attribute];
  }
  #assertPendingWriteForAttributeAndPeer(session, endpoint, cluster, attribute) {
    const attrWriteState = this.#pendingWriteStateForAttribute(endpoint, cluster, attribute);
    if (attrWriteState === void 0) {
      throw new StatusResponse.InvalidInStateError("There is no atomic write in progress for this attribute");
    }
    const peerAddress = this.#derivePeerAddress(session);
    if (peerAddress === void 0) {
      throw new StatusResponse.InvalidInStateError("There is no atomic write in progress for this peer");
    }
    if (!PeerAddress.is(attrWriteState.peerAddress, peerAddress)) {
      throw new StatusResponse.BusyError("Attribute is part of an atomic write in progress for a different peer");
    }
    return attrWriteState;
  }
  #derivePeerAddress(session) {
    if (hasRemoteActor(session) && Subject.isNode(session.subject) && NodeId.isOperationalNodeId(session.subject.id)) {
      return PeerAddress({ fabricIndex: session.fabric, nodeId: NodeId(session.subject.id) });
    }
  }
  #assertValidPeer(context) {
    assertRemoteActor(context);
    const peerAddress = this.#derivePeerAddress(context);
    if (!context.session.associatedFabric || peerAddress === void 0) {
      throw new StatusResponse.InvalidCommandError("AtomicRequest requires an operational session");
    }
    return peerAddress;
  }
}
export {
  AtomicWriteHandler
};
//# sourceMappingURL=AtomicWriteHandler.js.map
