/**
 * @license
 * Copyright 2025-2026 Open Home Foundation
 * SPDX-License-Identifier: Apache-2.0
 */
import { Bytes, DnsRecordType, Logger } from "@matter/main";
import { MdnsService } from "@matter/main/protocol";
const logger = Logger.get("BorderRouterDiscovery");
const REGISTRY_MAX_ENTRIES = 256;
const INSTANCE_OBSERVER_CAP = 512;
const STALE_RETENTION_MS = 24 * 60 * 60 * 1e3;
const MESHCOP_TYPE_QNAME = "_meshcop._udp.local";
const TREL_TYPE_QNAME = "_trel._udp.local";
const MESHCOP_SUFFIX = "._meshcop._udp.local";
const TREL_SUFFIX = "._trel._udp.local";
class BorderRouterDiscovery {
  #env;
  #registry = /* @__PURE__ */ new Map();
  #instanceObservers = /* @__PURE__ */ new Map();
  #targetObservers = /* @__PURE__ */ new Map();
  #names;
  #injectedNames;
  #suffixFilter;
  #discoveredObserver;
  #started = false;
  /** Incremented on every start/stop. Lets a pending `await mdns.construction.ready`
   *  detect that `stop()` ran while it was suspended and abort partial setup. */
  #startGeneration = 0;
  #evictionWarnedThisCycle = false;
  constructor(env, names) {
    this.#env = env;
    this.#injectedNames = names;
  }
  async start() {
    if (this.#started) return;
    const gen = ++this.#startGeneration;
    this.#evictionWarnedThisCycle = false;
    let names;
    if (this.#injectedNames !== void 0) {
      names = this.#injectedNames;
    } else {
      try {
        const mdns = this.#env.get(MdnsService);
        await mdns.construction.ready;
        if (gen !== this.#startGeneration) return;
        names = mdns.names;
      } catch (e) {
        if (gen !== this.#startGeneration) return;
        logger.error("MDNS service unavailable; border router discovery inactive:", e);
        return;
      }
    }
    this.#started = true;
    this.#names = names;
    const suffixFilter = ({ name }) => {
      const lower = name.toLowerCase();
      return lower.endsWith(MESHCOP_SUFFIX) || lower.endsWith(TREL_SUFFIX);
    };
    this.#suffixFilter = suffixFilter;
    names.filters.add(suffixFilter, [MESHCOP_TYPE_QNAME, TREL_TYPE_QNAME]);
    const meshcopType = names.get(MESHCOP_TYPE_QNAME);
    const trelType = names.get(TREL_TYPE_QNAME);
    names.solicitor.solicit({ name: meshcopType, recordTypes: [DnsRecordType.PTR] });
    names.solicitor.solicit({ name: trelType, recordTypes: [DnsRecordType.PTR] });
    const observer = (name) => this.#onDiscovered(name);
    this.#discoveredObserver = observer;
    names.discovered.on(observer);
  }
  async stop() {
    this.#startGeneration++;
    if (!this.#started) return;
    this.#started = false;
    const names = this.#names;
    if (names !== void 0) {
      if (this.#discoveredObserver !== void 0) {
        names.discovered.off(this.#discoveredObserver);
      }
      if (this.#suffixFilter !== void 0) {
        names.filters.delete(this.#suffixFilter);
      }
    }
    this.#discoveredObserver = void 0;
    this.#suffixFilter = void 0;
    for (const tracking of this.#instanceObservers.values()) {
      tracking.name.off(tracking.observer);
    }
    this.#instanceObservers.clear();
    for (const tracking of this.#targetObservers.values()) {
      tracking.target.off(tracking.observer);
    }
    this.#targetObservers.clear();
    this.#registry.clear();
    this.#names = void 0;
  }
  list() {
    this.#pruneExpired();
    return Array.from(this.#registry.values(), (entry) => this.#snapshotEntry(entry));
  }
  get(extAddressHex) {
    this.#pruneExpired();
    const entry = this.#registry.get(extAddressHex.toUpperCase());
    return entry === void 0 ? void 0 : this.#snapshotEntry(entry);
  }
  #pruneExpired() {
    const cutoff = Date.now() - STALE_RETENTION_MS;
    for (const [xaKey, entry] of this.#registry) {
      if (entry.sources.length === 0 && entry.lastSeen < cutoff) {
        this.#registry.delete(xaKey);
      }
    }
  }
  /** Shallow copy so callers cannot mutate registry state through the returned reference. */
  #snapshotEntry(entry) {
    return {
      ...entry,
      sources: [...entry.sources],
      addresses: [...entry.addresses]
    };
  }
  #onDiscovered(name) {
    if (!this.#started) return;
    this.#pruneExpired();
    const lower = name.qname.toLowerCase();
    if (lower === MESHCOP_TYPE_QNAME || lower === TREL_TYPE_QNAME) {
      return;
    }
    let source;
    if (lower.endsWith(MESHCOP_SUFFIX)) {
      source = "meshcop";
    } else if (lower.endsWith(TREL_SUFFIX)) {
      source = "trel";
    } else {
      return;
    }
    const key = lower;
    if (this.#instanceObservers.has(key)) {
      return;
    }
    if (this.#instanceObservers.size >= INSTANCE_OBSERVER_CAP) {
      while (this.#instanceObservers.size >= INSTANCE_OBSERVER_CAP) {
        if (!this.#evictOldestPendingInstance() && !this.#evictOldest()) break;
      }
    }
    const observer = () => this.#onInstanceChanged(name, source);
    this.#instanceObservers.set(key, { name, source, observer, firstSeen: Date.now() });
    name.on(observer);
    this.#parseAndUpsert(name, source);
  }
  /**
   * Evict the oldest xa-less instance observer when the observer cap is hit. Instances
   * that never publish a valid `xa` (malformed broadcasters or hostile noise) would
   * otherwise pin observers in `#instanceObservers` indefinitely — eviction targets only
   * those because observers tied to real registry entries are managed by `#evictOldest`.
   */
  #evictOldestPendingInstance() {
    let oldestKey;
    let oldestSeen = Number.POSITIVE_INFINITY;
    for (const [k, t] of this.#instanceObservers) {
      if (t.xaKey !== void 0) continue;
      if (t.firstSeen < oldestSeen) {
        oldestSeen = t.firstSeen;
        oldestKey = k;
      }
    }
    if (oldestKey === void 0) return false;
    const tracking = this.#instanceObservers.get(oldestKey);
    if (tracking === void 0) return false;
    tracking.name.off(tracking.observer);
    if (tracking.targetKey !== void 0) {
      this.#releaseTarget(tracking.targetKey);
    }
    this.#instanceObservers.delete(oldestKey);
    return true;
  }
  #onInstanceChanged(name, source) {
    if (!this.#started) return;
    if (name.isDiscovered) {
      try {
        this.#parseAndUpsert(name, source);
      } catch (e) {
        logger.debug("Error processing border router instance change:", e);
      }
      return;
    }
    const key = name.qname.toLowerCase();
    const tracking = this.#instanceObservers.get(key);
    if (tracking === void 0) {
      return;
    }
    tracking.name.off(tracking.observer);
    this.#instanceObservers.delete(key);
    if (tracking.targetKey !== void 0) {
      this.#releaseTarget(tracking.targetKey);
    }
    const xaKey = tracking.xaKey;
    if (xaKey === void 0) {
      return;
    }
    const entry = this.#registry.get(xaKey);
    if (entry === void 0) {
      return;
    }
    const idx = entry.sources.indexOf(source);
    if (idx !== -1) {
      entry.sources.splice(idx, 1);
    }
  }
  #onTargetChanged(target) {
    if (!this.#started) return;
    try {
      const targetQname = target.qname.toLowerCase();
      for (const entry of this.#registry.values()) {
        if (entry.hostname?.toLowerCase() === targetQname) {
          entry.addresses = this.#sortAddresses(this.#collectAddresses(target));
        }
      }
    } catch (e) {
      logger.debug("Error processing border router target change:", e);
    }
  }
  #parseAndUpsert(name, source) {
    const names = this.#names;
    if (names === void 0) return;
    try {
      const params = name.parameters;
      const xaKey = rawHex(params.raw("xa"), 8);
      if (xaKey === void 0) {
        return;
      }
      const records = Array.from(name.records);
      const srvRecord = records.find((r) => r.recordType === DnsRecordType.SRV);
      let srvTarget;
      let srvPort;
      if (srvRecord !== void 0 && isSrvValue(srvRecord.value)) {
        srvTarget = srvRecord.value.target;
        srvPort = srvRecord.value.port;
      }
      let addresses = [];
      const tracking = this.#instanceObservers.get(name.qname.toLowerCase());
      if (srvTarget !== void 0) {
        const target = names.get(srvTarget);
        addresses = this.#sortAddresses(this.#collectAddresses(target));
        this.#attachTargetObserver(name, srvTarget, target);
      } else if (tracking?.targetKey !== void 0) {
        this.#releaseTarget(tracking.targetKey);
        tracking.targetKey = void 0;
      }
      const xp = rawHex(params.raw("xp"), 8);
      const existing = this.#registry.get(xaKey);
      const meshcopWins = source === "meshcop";
      const entry = existing ?? {
        extAddressHex: xaKey,
        addresses: [],
        sources: [],
        lastSeen: Date.now()
      };
      const meshcopAlreadyContributed = entry.sources.includes("meshcop");
      const canOverwrite = meshcopWins || !meshcopAlreadyContributed;
      if (xp !== void 0 && canOverwrite) {
        entry.extendedPanIdHex = xp;
      }
      const previousHostname = entry.hostname;
      if (srvTarget !== void 0 && canOverwrite) {
        entry.hostname = srvTarget;
      }
      if (source === "meshcop" && srvTarget !== void 0 && previousHostname !== void 0 && previousHostname.toLowerCase() !== srvTarget.toLowerCase()) {
        this.#repointTrelTargetForXa(xaKey, previousHostname, srvTarget, names);
      }
      if (addresses.length > 0 && canOverwrite) {
        entry.addresses = addresses;
      }
      if (source === "meshcop") {
        if (srvPort !== void 0) entry.meshcopPort = srvPort;
        const nn = params.get("nn");
        if (nn !== void 0) entry.networkName = nn;
        const vn = params.get("vn");
        if (vn !== void 0) entry.vendorName = vn;
        const mn = params.get("mn");
        if (mn !== void 0) entry.modelName = mn;
        const tv = params.get("tv");
        if (tv !== void 0) entry.threadVersion = tv;
        const dd = rawHex(params.raw("dd"));
        if (dd !== void 0) entry.borderAgentIdHex = dd;
        const sb = rawHex(params.raw("sb"), 4);
        if (sb !== void 0) entry.stateBitmapHex = sb;
        const at = rawHex(params.raw("at"), 8);
        if (at !== void 0) entry.activeTimestampHex = at;
        const pt = rawHex(params.raw("pt"), 4);
        if (pt !== void 0) entry.partitionIdHex = pt;
        const dn = params.get("dn");
        if (dn !== void 0) entry.domainName = dn;
      } else if (source === "trel") {
        if (srvPort !== void 0) entry.trelPort = srvPort;
      }
      if (!entry.sources.includes(source)) {
        if (source === "meshcop") {
          entry.sources.unshift(source);
        } else {
          entry.sources.push(source);
        }
      }
      entry.lastSeen = Date.now();
      if (existing === void 0) {
        if (this.#registry.size >= REGISTRY_MAX_ENTRIES) {
          this.#evictOldest();
        }
        this.#registry.set(xaKey, entry);
      }
      if (tracking !== void 0) {
        tracking.xaKey = xaKey;
      }
    } catch (e) {
      logger.debug("Error parsing border router record:", e);
    }
  }
  #collectAddresses(target) {
    const out = new Array();
    for (const record of target.records) {
      if (record.recordType !== DnsRecordType.A && record.recordType !== DnsRecordType.AAAA) continue;
      if (typeof record.value === "string") {
        out.push(record.value);
      }
    }
    return out;
  }
  #sortAddresses(addresses) {
    const seen = /* @__PURE__ */ new Set();
    const unique = new Array();
    for (const addr of addresses) {
      if (!seen.has(addr)) {
        seen.add(addr);
        unique.push(addr);
      }
    }
    const ipv4 = unique.filter((a) => !a.includes(":"));
    const ipv6 = unique.filter((a) => a.includes(":"));
    const categorize = (a) => {
      const lower = a.toLowerCase();
      if (lower.startsWith("fe80:")) return 2;
      if (lower.startsWith("fc") || lower.startsWith("fd")) return 1;
      const firstChar = lower.charAt(0);
      if (firstChar === "2" || firstChar === "3") return 0;
      return 3;
    };
    ipv6.sort((a, b) => {
      const ca = categorize(a);
      const cb = categorize(b);
      if (ca !== cb) return ca - cb;
      return a.localeCompare(b);
    });
    ipv4.sort((a, b) => a.localeCompare(b));
    return [...ipv4, ...ipv6];
  }
  #attachTargetObserver(instance, srvTarget, target) {
    const targetKey = srvTarget.toLowerCase();
    const instanceKey = instance.qname.toLowerCase();
    const instanceTracking = this.#instanceObservers.get(instanceKey);
    const previousTargetKey = instanceTracking?.targetKey;
    if (previousTargetKey === targetKey) {
      return;
    }
    if (previousTargetKey !== void 0) {
      this.#releaseTarget(previousTargetKey);
    }
    const existing = this.#targetObservers.get(targetKey);
    if (existing !== void 0) {
      existing.refcount++;
    } else {
      const observer = () => this.#onTargetChanged(target);
      target.on(observer);
      this.#targetObservers.set(targetKey, { target, observer, refcount: 1 });
    }
    if (instanceTracking !== void 0) {
      instanceTracking.targetKey = targetKey;
    }
  }
  #repointTrelTargetForXa(xaKey, previousHostname, newTarget, names) {
    const previousKey = previousHostname.toLowerCase();
    for (const tracking of this.#instanceObservers.values()) {
      if (tracking.xaKey !== xaKey) continue;
      if (tracking.source !== "trel") continue;
      if (tracking.targetKey !== previousKey) continue;
      this.#attachTargetObserver(tracking.name, newTarget, names.get(newTarget));
    }
  }
  #releaseTarget(targetKey) {
    const tracking = this.#targetObservers.get(targetKey);
    if (tracking === void 0) return;
    tracking.refcount--;
    if (tracking.refcount <= 0) {
      tracking.target.off(tracking.observer);
      this.#targetObservers.delete(targetKey);
    }
  }
  #evictOldest() {
    let oldestStaleKey;
    let oldestStaleSeen = Number.POSITIVE_INFINITY;
    let oldestLiveKey;
    let oldestLiveSeen = Number.POSITIVE_INFINITY;
    for (const [xa, entry] of this.#registry) {
      if (entry.sources.length === 0) {
        if (entry.lastSeen < oldestStaleSeen) {
          oldestStaleSeen = entry.lastSeen;
          oldestStaleKey = xa;
        }
      } else if (entry.lastSeen < oldestLiveSeen) {
        oldestLiveSeen = entry.lastSeen;
        oldestLiveKey = xa;
      }
    }
    const evictKey = oldestStaleKey ?? oldestLiveKey;
    if (evictKey === void 0) return false;
    this.#registry.delete(evictKey);
    let releasedObservers = 0;
    for (const [instanceKey, tracking] of [...this.#instanceObservers]) {
      if (tracking.xaKey !== evictKey) continue;
      tracking.name.off(tracking.observer);
      if (tracking.targetKey !== void 0) {
        this.#releaseTarget(tracking.targetKey);
      }
      this.#instanceObservers.delete(instanceKey);
      releasedObservers++;
    }
    if (!this.#evictionWarnedThisCycle) {
      this.#evictionWarnedThisCycle = true;
      logger.warn(
        `Border router registry exceeded ${REGISTRY_MAX_ENTRIES} entries; evicting oldest (released ${releasedObservers} instance observer${releasedObservers === 1 ? "" : "s"})`
      );
    } else {
      logger.debug(`Evicted border router xa=${evictKey}; released ${releasedObservers} instance observers`);
    }
    return true;
  }
}
function isSrvValue(value) {
  return typeof value === "object" && value !== null && "target" in value && typeof value.target === "string" && "port" in value && typeof value.port === "number";
}
function rawHex(bytes, expectedByteLength) {
  if (bytes === void 0 || bytes.byteLength === 0) return void 0;
  if (expectedByteLength !== void 0 && bytes.byteLength !== expectedByteLength) return void 0;
  return Bytes.toHex(bytes).toUpperCase();
}
export {
  BorderRouterDiscovery
};
//# sourceMappingURL=BorderRouterDiscovery.js.map
