var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
var __typeError = (msg) => {
  throw TypeError(msg);
};
var __using = (stack, value, async) => {
  if (value != null) {
    if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
    var dispose, inner;
    if (async) dispose = value[__knownSymbol("asyncDispose")];
    if (dispose === void 0) {
      dispose = value[__knownSymbol("dispose")];
      if (async) inner = dispose;
    }
    if (typeof dispose !== "function") __typeError("Object not disposable");
    if (inner) dispose = function() {
      try {
        inner.call(this);
      } catch (e) {
        return Promise.reject(e);
      }
    };
    stack.push([async, dispose, value]);
  } else if (async) {
    stack.push([async]);
  }
  return value;
};
var __callDispose = (stack, error, hasError) => {
  var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
    return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
  };
  var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
  var next = (it) => {
    while (it = stack.pop()) {
      try {
        var result = it[1] && it[1].call(it[2]);
        if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
      } catch (e) {
        fail(e);
      }
    }
    if (hasError) throw error;
  };
  return next();
};
/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { DiscoveryData } from "#common/Scanner.js";
import { ChannelStatusResponseError } from "#securechannel/SecureChannelMessenger.js";
import { CaseClient } from "#session/case/CaseClient.js";
import {
  Abort,
  AbortedError,
  asError,
  BasicMultiplex,
  causedBy,
  ChannelType,
  Diagnostic,
  Duration,
  Heap,
  Lifetime,
  Logger,
  Millis,
  NetworkError,
  ServerAddress,
  ServerAddressSet,
  Time,
  Timestamp
} from "@matter/general";
import { GeneralStatusCode, SECURE_CHANNEL_PROTOCOL_ID, SecureChannelStatusCode } from "@matter/types";
import { TcpUnsupportedError, TransientPeerCommunicationError } from "./PeerCommunicationError.js";
import { PeerTimingParameters } from "./PeerTimingParameters.js";
const logger = Logger.get("PeerConnection");
async function PeerConnection(peer, context, options) {
  var _stack = [];
  try {
    const via = Diagnostic.via(peer.address.toString());
    const timing = options?.timing ? PeerTimingParameters.merge(context.timing, options.timing) : context.timing;
    const requiredTransport = options?.requiredTransport;
    const preferredTransport = options?.preferredTransport;
    const resolveTransports = () => peer.resolveTransports(requiredTransport, preferredTransport);
    const overallAbort = __using(_stack, new Abort(options));
    const lifetime = __using(_stack, (peer.lifetime ?? Lifetime.process).join("connecting"));
    let network = context.networks.select(peer, options?.network);
    const mediumProfile = context.networks.forPeer(peer);
    const peerAdditionalMrpDelay = options?.additionalMrpDelay ?? mediumProfile.connect?.additionalMrpDelay ?? mediumProfile.additionalMrpDelay;
    if (network.connect) {
      network = network.connect;
    }
    const _slot = __using(_stack, await network.semaphore.obtainSlot(overallAbort));
    if (overallAbort.aborted) {
      return;
    }
    peer.service.status.connecting(overallAbort.then(() => !!peer.sessions.size));
    const service = peer.service;
    const attempts = /* @__PURE__ */ new Map();
    let outputSession;
    let fatalError;
    const workers = new BasicMultiplex();
    const addresses = ServerAddressSet();
    const pendingAddresses = new Heap((a, b) => {
      const primary = ServerAddressSet.compareDesirability(a, b);
      const transports = resolveTransports();
      if (primary !== 0 || transports === void 0) {
        return primary;
      }
      return transportIndex(a, transports) - transportIndex(b, transports);
    }, addresses.add.bind(addresses));
    function transportIndex(address, transports) {
      const type = address.type;
      return type === void 0 ? -1 : transports.indexOf(type);
    }
    let attemptingFallback;
    let lastAttemptAt;
    let addrsAttempted = 0;
    const kicker = options?.kicker;
    let lastRestartAt;
    workers.add(scheduleAttempts());
    maybeAttemptFallback();
    for await (const { kind, address } of service.addressChanges({ abort: overallAbort })) {
      switch (kind) {
        case "add":
          addAddress(address);
          break;
        case "delete":
          deleteAddress(address, "Aborting attempt because address is expired");
          maybeAttemptFallback();
          break;
      }
    }
    if (outputSession) {
      peer.service.status.isReachable = true;
    }
    overallAbort();
    await workers;
    if (fatalError) {
      throw fatalError;
    }
    return outputSession;
    async function scheduleAttempts() {
      var _stack4 = [];
      try {
        const scheduling = __using(_stack4, lifetime.join("scheduling"));
        while (true) {
          if (!pendingAddresses.size) {
            var _stack2 = [];
            try {
              const _waiting = __using(_stack2, scheduling.join("waiting for address"));
              await overallAbort.race(pendingAddresses.added);
            } catch (_2) {
              var _error2 = _2, _hasError2 = true;
            } finally {
              __callDispose(_stack2, _error2, _hasError2);
            }
          }
          if (overallAbort.aborted) {
            return;
          }
          if (lastAttemptAt !== void 0) {
            const timeSinceLastAttempt = Timestamp.delta(lastAttemptAt);
            const delayInterval = Millis(timing.delayBeforeNextAddress - timeSinceLastAttempt);
            if (delayInterval > 0) {
              var _stack3 = [];
              try {
                const _delaying = __using(_stack3, scheduling.join("delaying"));
                const changed = await overallAbort.race(
                  Abort.sleep("connection delay", overallAbort, delayInterval),
                  pendingAddresses.added,
                  pendingAddresses.deleted
                );
                if (overallAbort.aborted) {
                  return;
                }
                if (changed !== void 0) {
                  continue;
                }
              } catch (_3) {
                var _error3 = _3, _hasError3 = true;
              } finally {
                __callDispose(_stack3, _error3, _hasError3);
              }
            }
          }
          const address = pendingAddresses.shift();
          if (address) {
            initiateAttempt(address);
          }
        }
      } catch (_4) {
        var _error4 = _4, _hasError4 = true;
      } finally {
        __callDispose(_stack4, _error4, _hasError4);
      }
    }
    function expandAddresses(address) {
      const transports = resolveTransports();
      if (transports === void 0) {
        return [address];
      }
      return transports.map(
        (type) => type === ChannelType.TCP ? { ...address, type } : { ...address, type }
      );
    }
    function addAddress(address) {
      for (const variant of expandAddresses(address)) {
        const interned = addresses.add(variant);
        const attempt = attempts.get(interned);
        if (attempt !== void 0) {
          if (attemptingFallback && ServerAddress.isEqual(attemptingFallback, interned)) {
            attemptingFallback = void 0;
            kicker?.emit("discover");
          }
          continue;
        }
        pendingAddresses.add(interned);
      }
    }
    function maybeAttemptFallback() {
      if (attempts.size || pendingAddresses.size || service.addresses.size) {
        return;
      }
      const fallback = peer.descriptor.operationalAddress;
      if (!fallback) {
        attemptingFallback = void 0;
        return;
      }
      const variants = expandAddresses(fallback);
      attemptingFallback = addresses.add(variants[0]);
      for (const variant of variants) {
        pendingAddresses.add(variant);
      }
    }
    function initiateAttempt(address) {
      address = addresses.add(address);
      if (attempts.has(address)) {
        return;
      }
      const addressAbort = new Abort({ abort: overallAbort });
      lastAttemptAt = Time.nowMs;
      const finished = connect(address, addressAbort).finally(() => {
        try {
          if (attempts.get(address)?.finished === finished) {
            attempts.delete(address);
            maybeAttemptFallback();
          }
        } finally {
          addressAbort.close();
        }
      });
      attempts.set(address, { abort: addressAbort, finished });
      workers.add(finished);
    }
    function deleteAddress(address, why) {
      const variants = expandAddresses(address).map((v) => addresses.add(v));
      const operationalAddress = peer.descriptor.operationalAddress;
      const isOperational = operationalAddress !== void 0 && ServerAddress.isEqual(operationalAddress, address);
      const remainingNonOperational = attempts.size - variants.filter((v) => attempts.has(v)).length;
      if (isOperational && remainingNonOperational === 0) {
        attemptingFallback = variants[0];
        return;
      }
      for (const variant of variants) {
        const attempt = attempts.get(variant);
        if (attempt) {
          debug(via, variant, why);
          attempt.abort();
          attempts.delete(variant);
        }
        pendingAddresses.delete(variant);
      }
    }
    function error(address, ...message) {
      logger.error(logHeaderFor(address), ...message);
    }
    function warn(address, ...message) {
      logger.warn(logHeaderFor(address), ...message);
    }
    function notice(address, ...message) {
      logger.notice(logHeaderFor(address), ...message);
    }
    function info(via2, address, ...message) {
      logger.info(logHeaderFor(address, via2), ...message);
    }
    function debug(via2, address, ...message) {
      logger.debug(logHeaderFor(address, via2), ...message);
    }
    function logHeaderFor(address, localVia = via) {
      return [localVia, Diagnostic.strong(ServerAddress.urlFor(address))];
    }
    async function connect(address, addressAbort) {
      var _stack2 = [];
      try {
        const addrNo = ++addrsAttempted;
        let attemptNo = 1;
        const connecting = __using(_stack2, lifetime.join("attempt"));
        connecting.details.address = ServerAddress.urlFor(address);
        if (attemptingFallback && !ServerAddress.isEqual(address, attemptingFallback)) {
          deleteAddress(
            attemptingFallback,
            "Aborting attempt to last known address because device reports address change"
          );
          attemptingFallback = void 0;
        }
        while (!addressAbort.aborted) {
          try {
            await attemptOnce(address, addressAbort, connecting, addrNo, attemptNo++);
          } catch (e) {
            await handleConnectionError(asError(e), address, addressAbort, connecting);
          }
        }
      } catch (_2) {
        var _error2 = _2, _hasError2 = true;
      } finally {
        __callDispose(_stack2, _error2, _hasError2);
      }
    }
    async function attemptOnce(address, addressAbort, attemptLifetime, addrNo, attemptNo) {
      let socket;
      {
        var _stack2 = [];
        try {
          const _opening = __using(_stack2, attemptLifetime.join("opening socket"));
          socket = await context.openSocket(address, addressAbort);
          if (socket === void 0) {
            return;
          }
        } catch (_2) {
          var _error2 = _2, _hasError2 = true;
        } finally {
          __callDispose(_stack2, _error2, _hasError2);
        }
      }
      let socketOwned = true;
      try {
        var _stack4 = [];
        try {
          const isFallback = attemptingFallback && addrsAttempted > 1;
          const unsecuredSession = __using(_stack4, context.sessions.createUnsecuredSession({
            channel: socket,
            sessionParameters: peer.sessionParameters,
            isInitiator: true
          }), true);
          const exchange = __using(_stack4, PeerConnection.createExchange(
            peer,
            context.exchanges,
            unsecuredSession,
            network,
            peerAdditionalMrpDelay
          ), true);
          info(
            Diagnostic.via(`${peer.address.toString()}${exchange.via}`),
            address,
            "Connecting",
            Diagnostic.dict({
              "addr #": addrNo,
              "attempt #": attemptNo,
              "connect time": Duration.format(Timestamp.delta(lifetime.startedAt)),
              "addr time": Duration.format(Timestamp.delta(attemptLifetime.startedAt))
            }),
            Diagnostic.asFlags({
              [network.id]: true,
              fallback: attemptingFallback !== void 0 && ServerAddress.isEqual(address, attemptingFallback)
            })
          );
          const caseClient = new CaseClient(context.sessions);
          const fabric = context.sessions.fabricFor(peer.address);
          let kick;
          const localAbort = __using(_stack4, new Abort({ abort: addressAbort }));
          try {
            var _stack3 = [];
            try {
              const _pairing = __using(_stack3, attemptLifetime.join("pairing"));
              kick = kicker?.use((origin) => {
                if (exchange.retransmissionCount < context.timing.kickMinRetransmissions) {
                  return;
                }
                if (exchange.retransmissionRestartSaving < context.timing.kickMinRestartSaving) {
                  debug(via, address, `Suppressing "${origin}" kick, restart would save too little time`);
                  return;
                }
                const threshold = origin === "discover" ? context.timing.kickRestartCooldown.addressChange : context.timing.kickRestartCooldown.connect;
                if (lastRestartAt === void 0 || Timestamp.delta(lastRestartAt) >= threshold) {
                  info(via, address, `Restarting exchange on "${origin}" kick`);
                  lastRestartAt = Time.nowMs;
                  localAbort();
                } else {
                  debug(
                    via,
                    address,
                    `Suppressing "${origin}" kick, last restart was ${Duration.format(Timestamp.delta(lastRestartAt))} ago`
                  );
                }
              });
              const isTcp = "type" in address && address.type === "tcp";
              const { session } = await caseClient.pair(exchange, fabric, peer.address.nodeId, {
                ...options,
                abort: localAbort,
                caseAuthenticatedTags: peer.descriptor.caseAuthenticatedTags,
                maxInitialRetransmissions: Infinity,
                maxInitialRetransmissionTime: timing.maxDelayBetweenInitialContactRetries,
                initialRetransmissionTime: isFallback ? timing.maxDelayBetweenInitialContactRetries : void 0,
                validateSessionParameters: isTcp ? (params) => {
                  if (params.supportedTransports.tcpServer !== false) {
                    return;
                  }
                  const advertisedByMdns = (DiscoveryData(peer.service.parameters).T ?? peer.descriptor.discoveryData?.T)?.tcpServer === true;
                  if (advertisedByMdns) {
                    return;
                  }
                  peer.markTcpUnsupported();
                  throw new TcpUnsupportedError(
                    `Peer negotiated a TCP session but reports no TCP server support`
                  );
                } : void 0
              });
              outputSession = session;
              socketOwned = false;
              overallAbort();
            } catch (_3) {
              var _error3 = _3, _hasError3 = true;
            } finally {
              __callDispose(_stack3, _error3, _hasError3);
            }
          } catch (e) {
            if (AbortedError.is(e)) {
              return;
            }
            throw e;
          } finally {
            kick?.[Symbol.dispose]();
          }
        } catch (_4) {
          var _error4 = _4, _hasError4 = true;
        } finally {
          var _promise = __callDispose(_stack4, _error4, _hasError4);
          _promise && await _promise;
        }
      } finally {
        if (socketOwned) {
          try {
            await socket.close();
          } catch (e) {
            warn(address, "Error closing abandoned channel:", Diagnostic.errorMessage(asError(e)));
          }
        }
      }
    }
    async function handleConnectionError(e, address, addressAbort, lifetime2) {
      var _stack2 = [];
      try {
        const _handling = __using(_stack2, lifetime2.join("handling error"));
        if (causedBy(e, TcpUnsupportedError)) {
          if (requiredTransport === ChannelType.TCP) {
            error(address, "Peer reports no TCP support but TCP was required; aborting connection");
            fatalError = asError(e);
          } else {
            notice(address, "Peer negotiated a TCP session but reports no TCP support; falling back to UDP");
          }
          overallAbort();
          return;
        }
        let delay;
        let category;
        const csre = ChannelStatusResponseError.of(e);
        if (csre) {
          if (csre.generalStatusCode === GeneralStatusCode.Busy && csre.busyDelay !== void 0) {
            delay = Millis(csre.busyDelay + Math.round(Math.random() * timing.delayAfterNetworkError));
            category = "busy";
          } else if (csre.protocolStatusCode === SecureChannelStatusCode.NoSharedTrustRoots && await context.sessions.deleteResumptionRecord(peer.address)) {
            category = "resumption";
          } else {
            delay = timing.delayAfterPeerError;
            category = "peer";
          }
        } else if (causedBy(e, TransientPeerCommunicationError)) {
          delay = timing.delayAfterNetworkError;
          category = "network";
        } else {
          delay = timing.delayAfterUnhandledError;
          category = "general";
        }
        if (causedBy(e, NetworkError)) {
          peer.service.status.isReachable = false;
        }
        const handleError = options?.handleError ?? context.handleError;
        if (delay !== void 0 && handleError) {
          try {
            const result = handleError(e);
            if (result !== void 0) {
              delay = result;
            }
          } catch (thrown) {
            error(address, "Fatal peer error, aborting connection:", Diagnostic.errorMessage(asError(thrown)));
            fatalError = asError(thrown);
            overallAbort();
            return;
          }
        }
        switch (category) {
          case "busy":
            info(
              via,
              address,
              `Peer requested to delay and retry no earlier than ${Duration.format(csre.busyDelay)} from now (retry in ${Duration.format(delay)})`
            );
            break;
          case "resumption":
            warn(
              address,
              "Authorization rejected by peer on session resumption; clearing resumption data and retrying"
            );
            break;
          case "peer":
            warn(address, `Peer error (retry in ${Duration.format(delay)}):`, Diagnostic.errorMessage(e));
            break;
          case "network":
            warn(address, `Connection error (retry in ${Duration.format(delay)}):`, Diagnostic.errorMessage(e));
            break;
          case "general":
            warn(address, `General connection error (retry in ${Duration.format(delay)}):`, e);
            break;
        }
        if (addressAbort.aborted) {
          return;
        }
        if (delay) {
          await Abort.sleep("peer connection retry", addressAbort, delay);
        }
      } catch (_2) {
        var _error2 = _2, _hasError2 = true;
      } finally {
        __callDispose(_stack2, _error2, _hasError2);
      }
    }
  } catch (_) {
    var _error = _, _hasError = true;
  } finally {
    __callDispose(_stack, _error, _hasError);
  }
}
((PeerConnection2) => {
  function createExchange(peer, exchanges, session, network, peerAdditionalMrpDelay, protocol = SECURE_CHANNEL_PROTOCOL_ID, addressOverride) {
    return exchanges.initiateExchangeForSession(session, protocol, {
      onSend,
      onReceive,
      network,
      peerAdditionalMrpDelay,
      addressOverride
    });
    function onSend(_message, retransmission) {
      if (retransmission) {
        peer.service.status.isReachable = false;
      }
    }
    function onReceive() {
      peer.service.status.lastReceiptAt = Time.nowMs;
    }
  }
  PeerConnection2.createExchange = createExchange;
})(PeerConnection || (PeerConnection = {}));
export {
  PeerConnection
};
//# sourceMappingURL=PeerConnection.js.map
