/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  Abort,
  ChannelType,
  Diagnostic,
  DnsRecordType,
  IpService,
  MatterAggregateError,
  ObserverGroup,
  Time
} from "@matter/general";
import { VendorId } from "@matter/types";
import { DiscoveryData } from "../common/Scanner.js";
import {
  MATTER_COMMISSION_SERVICE_QNAME,
  getCommissionableDeviceQname,
  getCommissioningModeQname,
  getDeviceTypeQname,
  getLongDiscriminatorQname,
  getShortDiscriminatorQname,
  getVendorQname
} from "./MdnsConsts.js";
class CommissionableMdnsScanner {
  type = ChannelType.UDP;
  #names;
  #filter;
  #observers = new ObserverGroup();
  #cache = /* @__PURE__ */ new Map();
  #waiters = /* @__PURE__ */ new Set();
  constructor(names) {
    this.#names = names;
    const suffix1 = `._matterc._udp.local`;
    const base1 = `_matterc._udp.local`;
    const suffix2 = `._matterd._udp.local`;
    const base2 = `_matterd._udp.local`;
    this.#filter = (record) => {
      const lower = record.name.toLowerCase();
      return lower === base1 || lower.endsWith(suffix1) || lower === base2 || lower.endsWith(suffix2);
    };
    names.filters.add(this.#filter);
    this.#observers.on(names.discovered, this.#onDiscovered.bind(this));
  }
  async close() {
    for (const waiter of this.#waiters) {
      waiter.cancel();
    }
    this.#names.filters.delete(this.#filter);
    this.#observers.close();
    await MatterAggregateError.allSettled(
      [...this.#cache.values()].map(({ ipService }) => ipService.close()),
      "Error closing cached IpServices"
    );
    this.#cache.clear();
  }
  async findCommissionableDevicesContinuously(identifier, callback, timeout, cancelSignal) {
    for (const name of this.#names.discoveredNames) {
      this.#onDiscovered(name);
    }
    const seen = /* @__PURE__ */ new Set();
    const result = [];
    for (const cached of this.#cache.values()) {
      const device = refreshAddresses(cached);
      if (matchesIdentifier(device, identifier)) {
        seen.add(device.deviceIdentifier);
        result.push(device);
        callback(device);
      }
    }
    let cancelResolve;
    const internalCancel = new Promise((resolve) => cancelResolve = resolve);
    const waiter = {
      identifier,
      cancel: cancelResolve,
      notify: (device) => {
        if (matchesIdentifier(device, identifier) && !seen.has(device.deviceIdentifier)) {
          seen.add(device.deviceIdentifier);
          result.push(device);
          callback(device);
        }
      }
    };
    this.#waiters.add(waiter);
    const sleepTimer = timeout !== void 0 ? Time.sleep("commissionable scanner timeout", timeout) : void 0;
    const signals = [internalCancel];
    if (sleepTimer) signals.push(sleepTimer);
    if (cancelSignal) signals.push(cancelSignal);
    const queryAbort = new Abort();
    const discoveries = this.#startDiscovery(identifier, queryAbort);
    try {
      await Promise.race(signals);
      sleepTimer?.cancel();
    } finally {
      queryAbort();
      await discoveries;
      this.#waiters.delete(waiter);
    }
    return result;
  }
  getDiscoveredCommissionableDevices(identifier) {
    return [...this.#cache.values()].map((cached) => refreshAddresses(cached)).filter((device) => matchesIdentifier(device, identifier));
  }
  cancelCommissionableDeviceDiscovery(identifier) {
    for (const waiter of this.#waiters) {
      if (matchesWaiterIdentifier(waiter.identifier, identifier)) {
        waiter.cancel();
      }
    }
  }
  #onDiscovered(name) {
    const lower = name.qname.toLowerCase();
    if (!lower.endsWith("._matterc._udp.local") && !lower.endsWith("._matterd._udp.local")) {
      return;
    }
    if (this.#cache.has(lower)) {
      return;
    }
    const ipService = new IpService(name.qname, Diagnostic.via("commissionable-scanner"), this.#names);
    const device = buildCommissionableDevice(name);
    if (device !== void 0) {
      this.#cacheDevice(lower, device, ipService, name);
      return;
    }
    const pendingObserver = ({ name: changedName }) => {
      if (!changedName.isDiscovered) {
        this.#observers.off(name, pendingObserver);
        void ipService.close();
        return;
      }
      const built = buildCommissionableDevice(changedName);
      if (built !== void 0) {
        this.#observers.off(name, pendingObserver);
        this.#cacheDevice(lower, built, ipService, name);
      }
    };
    this.#observers.on(name, pendingObserver);
  }
  #cacheDevice(lower, device, ipService, name) {
    const observer = ({ name: changedName }) => {
      if (!changedName.isDiscovered) {
        const cached2 = this.#cache.get(lower);
        if (cached2) {
          this.#cache.delete(lower);
          this.#observers.off(name, cached2.observer);
          void cached2.ipService.close();
        }
      }
    };
    const cached = { device, ipService, name, observer };
    this.#cache.set(lower, cached);
    this.#observers.on(name, observer);
    for (const waiter of this.#waiters) {
      waiter.notify(refreshAddresses(cached));
    }
  }
  async #startDiscovery(identifier, abort) {
    const solicitor = this.#names.solicitor;
    const qnames = getQueryQnames(identifier);
    const discoveries = qnames.map(
      (qname) => solicitor.discover({
        name: this.#names.get(qname),
        recordTypes: [DnsRecordType.PTR],
        abort
      })
    );
    await MatterAggregateError.allSettled(discoveries);
  }
}
function buildCommissionableDevice(name) {
  const params = name.parameters;
  const D = Number(params.get("D"));
  const CM = Number(params.get("CM"));
  if (!isFinite(D) || !isFinite(CM)) {
    return void 0;
  }
  const instanceId = name.qname.split(".")[0];
  const dd = DiscoveryData(params);
  dd.T ??= 0;
  dd.ICD ??= 0;
  return {
    ...dd,
    deviceIdentifier: instanceId,
    D,
    CM,
    addresses: []
  };
}
function refreshAddresses(cached) {
  cached.device.addresses = [...cached.ipService.addresses];
  return cached.device;
}
function matchesIdentifier(device, identifier) {
  if ("instanceId" in identifier) {
    return device.deviceIdentifier === identifier.instanceId;
  }
  if ("longDiscriminator" in identifier) {
    return device.D === identifier.longDiscriminator;
  }
  if ("shortDiscriminator" in identifier) {
    return (device.D >> 8 & 15) === identifier.shortDiscriminator;
  }
  if ("vendorId" in identifier) {
    const vp = device.VP?.split("+");
    if (!vp) return false;
    const vendorMatch = VendorId(Number(vp[0])) === identifier.vendorId;
    if ("productId" in identifier && identifier.productId !== void 0) {
      return vendorMatch && Number(vp[1]) === identifier.productId;
    }
    return vendorMatch;
  }
  if ("deviceType" in identifier) {
    return device.DT === identifier.deviceType;
  }
  if ("productId" in identifier) {
    const vp = device.VP?.split("+");
    return vp ? Number(vp[1]) === identifier.productId : false;
  }
  return device.CM === 1 || device.CM === 2;
}
function matchesWaiterIdentifier(a, b) {
  const aKeys = Object.keys(a);
  const bKeys = Object.keys(b);
  if (aKeys.length !== bKeys.length) return false;
  for (const key of aKeys) {
    if (a[key] !== b[key]) return false;
  }
  return true;
}
function getQueryQnames(identifier) {
  const base = [MATTER_COMMISSION_SERVICE_QNAME];
  if ("instanceId" in identifier) {
    return [...base, getCommissionableDeviceQname(identifier.instanceId)];
  }
  if ("longDiscriminator" in identifier) {
    return [...base, getLongDiscriminatorQname(identifier.longDiscriminator)];
  }
  if ("shortDiscriminator" in identifier) {
    return [...base, getShortDiscriminatorQname(identifier.shortDiscriminator)];
  }
  if ("vendorId" in identifier) {
    return [...base, getVendorQname(identifier.vendorId)];
  }
  if ("deviceType" in identifier) {
    return [...base, getDeviceTypeQname(identifier.deviceType)];
  }
  return [...base, getCommissioningModeQname()];
}
export {
  CommissionableMdnsScanner
};
//# sourceMappingURL=CommissionableMdnsScanner.js.map
