/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Subject } from "#action/server/Subject.js";
import { MessageCodec, SessionType } from "#codec/MessageCodec.js";
import { Mark } from "#common/Mark.js";
import { PeerAddress } from "#peer/PeerAddress.js";
import { PeerInitiatedCloseError } from "#peer/PeerCommunicationError.js";
import { NoAssociatedFabricError } from "#protocol/errors.js";
import { MessageCounter } from "#protocol/MessageCounter.js";
import { MessageReceptionStateEncryptedWithoutRollover } from "#protocol/MessageReceptionState.js";
import {
  AsyncObservableValue,
  BasicSet,
  Bytes,
  CRYPTO_SYMMETRIC_KEY_LENGTH,
  Diagnostic,
  Duration,
  InternalError,
  Logger,
  MatterFlowError,
  hex
} from "@matter/general";
import { FabricIndex, GlobalFabricId, NodeId } from "@matter/types";
import { SecureSession } from "./SecureSession.js";
import { Session } from "./Session.js";
const logger = Logger.get("SecureSession");
const SESSION_KEYS_INFO = Bytes.fromString("SessionKeys");
const SESSION_RESUMPTION_KEYS_INFO = Bytes.fromString("SessionResumptionKeys");
class NodeSession extends SecureSession {
  #crypto;
  #id;
  #isInitiator;
  #fabric;
  #peerNodeId;
  #peerSessionId;
  #decryptKey;
  #encryptKey;
  #attestationKey;
  #caseAuthenticatedTags;
  supportsMRP = true;
  type = SessionType.Unicast;
  #closedByPeer = AsyncObservableValue();
  #isPeerLost = false;
  // TODO - remove this; subscriptions should be owned by the peer, not the session
  #subscriptions = new BasicSet();
  static async create(config) {
    const { manager, sharedSecret, salt, isResumption, peerSessionParameters, isInitiator } = config;
    if (manager) {
      await manager.construction;
    }
    const keys = Bytes.of(
      await config.crypto.createHkdfKey(
        sharedSecret,
        salt,
        isResumption ? SESSION_RESUMPTION_KEYS_INFO : SESSION_KEYS_INFO,
        CRYPTO_SYMMETRIC_KEY_LENGTH * 3
      )
    );
    const decryptKey = isInitiator ? keys.slice(16, 32) : keys.slice(0, 16);
    const encryptKey = isInitiator ? keys.slice(0, 16) : keys.slice(16, 32);
    const attestationKey = keys.slice(32, 48);
    return new this({
      ...config,
      decryptKey,
      encryptKey,
      attestationKey,
      sessionParameters: peerSessionParameters
    });
  }
  constructor(config) {
    const {
      crypto,
      manager,
      id,
      fabric,
      peerNodeId,
      peerSessionId,
      decryptKey,
      encryptKey,
      attestationKey,
      caseAuthenticatedTags,
      isInitiator,
      delayManagerRegistration
    } = config;
    super({
      ...config,
      setActiveTimestamp: true,
      // We always set the active timestamp for Secure sessions
      // Can be changed to a PersistedMessageCounter if we implement session storage
      messageCounter: new MessageCounter(crypto, async () => {
        await this.initiateClose(async () => {
          await this.closeSubscriptions(true);
        });
      }),
      messageReceptionState: new MessageReceptionStateEncryptedWithoutRollover(0)
    });
    this.#crypto = crypto;
    this.#id = id;
    this.#fabric = fabric;
    this.#peerNodeId = peerNodeId;
    this.#peerSessionId = peerSessionId;
    this.#decryptKey = decryptKey;
    this.#encryptKey = encryptKey;
    this.#attestationKey = attestationKey;
    this.#caseAuthenticatedTags = caseAuthenticatedTags ?? [];
    this.#isInitiator = isInitiator;
    logger.debug(
      `Created secure ${this.isPase ? "PASE" : "CASE"} session for fabric index ${fabric?.fabricIndex}`,
      this.via,
      this.parameterDiagnostics
    );
    if (!delayManagerRegistration) {
      manager?.sessions.add(this);
    }
    fabric?.addSession(this);
  }
  get parameterDiagnostics() {
    return Diagnostic.dict(
      {
        SII: Duration.format(this.idleInterval),
        SAI: Duration.format(this.activeInterval),
        SAT: Duration.format(this.activeThreshold),
        DMRev: this.dataModelRevision,
        IMRev: this.interactionModelRevision,
        spec: Diagnostic.hex(this.specificationVersion),
        maxPaths: this.maxPathsPerInvoke,
        CATs: this.#caseAuthenticatedTags
      },
      true
    );
  }
  get caseAuthenticatedTags() {
    return this.#caseAuthenticatedTags;
  }
  get isPase() {
    return this.#peerNodeId === NodeId.UNSPECIFIED_NODE_ID;
  }
  get subscriptions() {
    return this.#subscriptions;
  }
  get isInitiator() {
    return this.#isInitiator;
  }
  subjectFor(_message) {
    return Subject.Node({
      id: this.peerNodeId,
      catSubjects: this.#caseAuthenticatedTags.map((cat) => NodeId.fromCaseAuthenticatedTag(cat))
    });
  }
  decode({ header, applicationPayload, messageExtension }, aad) {
    if (header.hasMessageExtensions) {
      logger.info(
        `Message extensions are not supported. Ignoring ${messageExtension ? Bytes.toHex(messageExtension) : void 0}`
      );
    }
    const nonce = Session.generateNonce(header.securityFlags, header.messageId, this.#peerNodeId);
    const message = MessageCodec.decodePayload({
      header,
      applicationPayload: this.#crypto.decrypt(this.#decryptKey, applicationPayload, nonce, aad)
    });
    if (message.payloadHeader.hasSecuredExtension) {
      logger.info(
        `Secured extensions are not supported. Ignoring ${message.securityExtension ? Bytes.toHex(message.securityExtension) : void 0}`
      );
    }
    return message;
  }
  encode(message) {
    message.packetHeader.sessionId = this.#peerSessionId;
    const { header, applicationPayload } = MessageCodec.encodePayload(message);
    const headerBytes = MessageCodec.encodePacketHeader(message.packetHeader);
    const securityFlags = headerBytes[3];
    const sessionNodeId = this.isPase ? NodeId.UNSPECIFIED_NODE_ID : this.#fabric?.nodeId ?? NodeId.UNSPECIFIED_NODE_ID;
    const nonce = Session.generateNonce(securityFlags, header.messageId, sessionNodeId);
    return {
      header,
      applicationPayload: this.#crypto.encrypt(this.#encryptKey, applicationPayload, nonce, headerBytes)
    };
  }
  get attestationChallengeKey() {
    return this.#attestationKey;
  }
  get fabric() {
    return this.#fabric;
  }
  set fabric(fabric) {
    if (this.#fabric !== void 0 && this.#fabric !== fabric) {
      throw new InternalError("Cannot change session fabric");
    }
    this.#fabric = fabric;
    fabric.addSession(this);
  }
  get id() {
    return this.#id;
  }
  get via() {
    return Diagnostic.via(`${this.peerAddress.toString()}${Mark.SESSION}${hex.word(this.id)}`);
  }
  get peerSessionId() {
    return this.#peerSessionId;
  }
  get nodeId() {
    return this.#fabric?.nodeId ?? NodeId.UNSPECIFIED_NODE_ID;
  }
  get peerNodeId() {
    return this.#peerNodeId;
  }
  get hasAssociatedFabric() {
    return this.#fabric !== void 0;
  }
  get associatedFabric() {
    if (this.#fabric === void 0) {
      throw new NoAssociatedFabricError(
        `${this.isPase ? "PASE " : ""}Session needs to have an associated Fabric for fabric sensitive data handling.`
      );
    }
    return this.#fabric;
  }
  async closeSubscriptions(flush = false) {
    const subscriptions = [...this.#subscriptions];
    for (const subscription of subscriptions) {
      await subscription.close(flush ? this : void 0);
    }
    return subscriptions.length;
  }
  get closedByPeer() {
    return this.#closedByPeer;
  }
  async handlePeerClose(currentExchange) {
    this.#isPeerLost = true;
    await this.#closedByPeer.emit(true);
    await this.handlePeerLoss({ cause: new PeerInitiatedCloseError(), currentExchange });
  }
  async handlePeerLoss(context) {
    this.#isPeerLost = true;
    await this.initiateForceClose(context);
  }
  get isPeerLost() {
    return this.#isPeerLost;
  }
  /**
   * Close the session.
   *
   * If there are open subscriptions they are closed without flushing.  To flush, use {@link closeSubscriptions}
   * before invoking.
   *
   * TODO - subscription handling is only relevant for server; needs to move to server-specific component
   *
   * If there are active exchanges the close is deferred until the final exchange closes.  To close sooner, use
   * {@link initiateForceClose}.
   *
   * A final exchange will be opened to notify the peer of closure unless the peer is marked as lost.
   */
  async initiateClose(shutdownLogic) {
    await super.initiateClose(async () => {
      await shutdownLogic?.();
      if (this.hasActiveExchanges) {
        logger.debug(this.via, "Session ends when exchanges end");
        this.deferredClose = true;
      }
    });
  }
  async initiateForceClose(context) {
    this.#isPeerLost = true;
    await super.initiateForceClose(context);
  }
  addExchange(exchange) {
    super.addExchange(exchange);
    exchange.closed.on(async () => {
      this.exchanges.delete(exchange);
      if (this.deferredClose && !this.hasActiveExchanges) {
        this.deferredClose = false;
        await this.close();
      }
    });
  }
  async close() {
    if (!this.#isPeerLost) {
      try {
        await this.gracefulClose.emit();
      } catch (e) {
        logger.error(`Unhandled error in ${this.via} graceful close handler:`, e);
      }
    }
    await this.closeSubscriptions();
    this.#fabric?.deleteSession(this);
    await super.close();
    this.manager?.sessions.delete(this);
  }
  /**
   * The peer node's address.
   */
  get peerAddress() {
    return PeerAddress({
      fabricIndex: this.#fabric?.fabricIndex ?? FabricIndex.NO_FABRIC,
      nodeId: this.#peerNodeId
    });
  }
  /**
   * Indicates whether a peer matches a specific address.
   */
  peerIs(address) {
    return (this.#fabric?.fabricIndex ?? FabricIndex.NO_FABRIC) === address.fabricIndex && this.#peerNodeId === address.nodeId;
  }
}
((NodeSession2) => {
  function assert(session, errorText) {
    if (!is(session)) {
      throw new MatterFlowError(errorText ?? "Unsecured session in secure context");
    }
  }
  NodeSession2.assert = assert;
  function is(session) {
    return session?.type === SessionType.Unicast;
  }
  NodeSession2.is = is;
  function logNew(logger2, operation, session, messenger, fabric, peerNodeId) {
    logger2.info(
      session.via,
      `${operation} session with`,
      Diagnostic.strong(PeerAddress({ fabricIndex: fabric.fabricIndex, nodeId: peerNodeId }).toString()),
      messenger.exchange.diagnostics,
      Diagnostic.dict({
        address: messenger.channelName,
        fabric: `${GlobalFabricId.strOf(fabric.globalId)} (#${fabric.fabricIndex})`,
        ...session.parameterDiagnostics
      })
    );
  }
  NodeSession2.logNew = logNew;
})(NodeSession || (NodeSession = {}));
export {
  NodeSession
};
//# sourceMappingURL=NodeSession.js.map
