/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  DnsCodec,
  DnsMessageType,
  DnsMessageTypeFlag,
  MAX_MDNS_MESSAGE_SIZE
} from "#codec/DnsCodec.js";
import { Logger } from "#log/Logger.js";
import { MatterAggregateError } from "#MatterError.js";
import { UdpMulticastServer } from "#net/udp/UdpMulticastServer.js";
import { BasicObservable } from "#util/Observable.js";
import { MaybePromise } from "#util/Promises.js";
import { MdnsRelevanceFilter } from "./MdnsRelevanceFilter.js";
const logger = Logger.get("MdnsListener");
const DNS_HEADER_SIZE = 12;
class MdnsSocket {
  #socket;
  #handlers;
  #isClosed = false;
  #receipt = new BasicObservable(
    (error) => logger.error("Unhandled error in MDNS listener", error),
    true
  );
  #relevance = new MdnsRelevanceFilter();
  static async create(network, options) {
    const { enableIpv4 = true, netInterface, lifetime } = options ?? {};
    const socket = new MdnsSocket(
      await UdpMulticastServer.create({
        lifetime,
        network,
        netInterface,
        broadcastAddressIpv4: enableIpv4 ? MdnsSocket.BROADCAST_IPV4 : void 0,
        broadcastAddressIpv6: MdnsSocket.BROADCAST_IPV6,
        listeningPort: MdnsSocket.BROADCAST_PORT
      })
    );
    return socket;
  }
  constructor(socket) {
    this.#socket = socket;
    socket.onMessage(this.#handleMessage.bind(this));
  }
  get network() {
    return this.#socket.network;
  }
  get supportsIpv4() {
    return this.#socket.supportsIpv4;
  }
  get netInterface() {
    return this.#socket.netInterface;
  }
  get receipt() {
    return this.#receipt;
  }
  /**
   * Declare the DNS names an owner cares about so irrelevant packets are dropped before the expensive full decode.
   * See {@link MdnsRelevanceFilter}; pass `"all"` when interest cannot be enumerated. For interest that changes one
   * name at a time, prefer {@link addRelevantName} / {@link removeRelevantName}.
   */
  registerRelevantNames(owner, names) {
    this.#relevance.registerRelevantNames(owner, names);
  }
  addRelevantName(owner, name) {
    this.#relevance.addRelevantName(owner, name);
  }
  removeRelevantName(owner, name) {
    this.#relevance.removeRelevantName(owner, name);
  }
  unregisterRelevantNames(owner) {
    this.#relevance.unregisterRelevantNames(owner);
  }
  async send(message, intf, unicastDest) {
    const { messageType, queries = [] } = message;
    const queryChunks = this.#splitQueries(queries);
    if (queryChunks.length > 1) {
      for (const queryChunk of queryChunks) {
        const chunkMessage = {
          transactionId: 0,
          messageType,
          queries: queryChunk,
          answers: [],
          authorities: message.authorities ?? [],
          additionalRecords: []
        };
        await this.#send(chunkMessage, intf, unicastDest);
      }
      return;
    }
    const truncatedMessageType = DnsMessageType.isQuery(messageType) ? messageType | DnsMessageTypeFlag.TC : messageType;
    const chunk = {
      transactionId: 0,
      queries: [],
      authorities: [],
      ...message,
      answers: [],
      additionalRecords: []
    };
    let encodedChunkWithoutAnswers = DnsCodec.encode(chunk);
    let chunkSize = encodedChunkWithoutAnswers.byteLength;
    for (const answer of message.answers ?? []) {
      const answerEncoded = DnsCodec.encodeRecord(answer);
      if (chunkSize + answerEncoded.byteLength > MAX_MDNS_MESSAGE_SIZE) {
        await this.#send(
          {
            ...chunk,
            messageType: truncatedMessageType
          },
          intf,
          unicastDest
        );
        if (chunk.queries.length) {
          chunk.queries.length = 0;
          encodedChunkWithoutAnswers = DnsCodec.encode(chunk);
        }
        chunk.answers.length = 0;
        chunkSize = encodedChunkWithoutAnswers.byteLength + answerEncoded.byteLength;
      } else {
        chunkSize += answerEncoded.byteLength;
      }
      chunk.answers.push(answerEncoded);
    }
    for (const additionalRecord of message.additionalRecords ?? []) {
      const additionalRecordEncoded = DnsCodec.encodeRecord(additionalRecord);
      if (chunkSize + additionalRecordEncoded.byteLength > MAX_MDNS_MESSAGE_SIZE) {
        await this.#send(chunk, intf, unicastDest);
        chunk.queries.length = 0;
        chunk.answers.length = 0;
        chunk.additionalRecords.length = 0;
        chunkSize = DnsCodec.encode(chunk).byteLength + additionalRecordEncoded.byteLength;
      } else {
        chunkSize += additionalRecordEncoded.byteLength;
      }
      chunk.additionalRecords.push(additionalRecordEncoded);
    }
    await this.#send(chunk, intf, unicastDest);
  }
  /**
   * Split queries into chunks that fit within MAX_MDNS_MESSAGE_SIZE.
   * Returns an array of query arrays - if all queries fit in one message, returns [[...queries]].
   */
  #splitQueries(queries) {
    if (queries.length === 0) {
      return [[]];
    }
    const chunks = [];
    let currentChunk = [];
    let currentChunkSize = DNS_HEADER_SIZE;
    for (const query of queries) {
      const querySize = DnsCodec.encodeQuery(query).byteLength;
      if (currentChunkSize + querySize > MAX_MDNS_MESSAGE_SIZE && currentChunk.length > 0) {
        chunks.push(currentChunk);
        currentChunk = [];
        currentChunkSize = DNS_HEADER_SIZE;
      }
      currentChunk.push(query);
      currentChunkSize += querySize;
    }
    if (currentChunk.length > 0) {
      chunks.push(currentChunk);
    }
    return chunks;
  }
  async #send(message, intf, unicastDest) {
    await this.#socket.send(DnsCodec.encode(message), intf, unicastDest);
  }
  async close() {
    this.#isClosed = true;
    await this.#socket.close();
    if (this.#handlers) {
      await MatterAggregateError.allSettled(this.#handlers);
    }
  }
  #handleMessage(bytes, sourceIp, sourceIntf) {
    if (this.#isClosed) {
      return;
    }
    if (!this.#relevance.isRelevant(bytes)) {
      return;
    }
    const parsed = DnsCodec.decode(bytes);
    if (parsed === void 0) {
      return;
    }
    let promise = this.#receipt.emit({
      ...parsed,
      sourceIp,
      sourceIntf
    });
    if (MaybePromise.is(promise)) {
      if (this.#handlers === void 0) {
        this.#handlers = /* @__PURE__ */ new Set();
      }
      promise = Promise.resolve(promise).finally(() => this.#handlers?.delete(promise));
    }
  }
}
((MdnsSocket2) => {
  MdnsSocket2.BROADCAST_IPV4 = "224.0.0.251";
  MdnsSocket2.BROADCAST_IPV6 = "ff02::fb";
  MdnsSocket2.BROADCAST_PORT = 5353;
})(MdnsSocket || (MdnsSocket = {}));
export {
  MdnsSocket
};
//# sourceMappingURL=MdnsSocket.js.map
