/**
 * @license
 * Copyright 2025-2026 Open Home Foundation
 * SPDX-License-Identifier: Apache-2.0
 */
import { cdSigners, paaRoots, vendors } from "@matter/dcl-data/node";
import {
  CommissioningClient,
  Crypto,
  DclBehavior,
  FabricId,
  GlobalFabricId,
  Logger,
  MatterAggregateError,
  NodeId,
  SoftwareUpdateManager,
  Timestamp
} from "@matter/main";
import { DclCertificateService, DclVendorInfoService } from "@matter/main/protocol";
import { VendorId } from "@matter/main/types";
import { Endpoint } from "@matter/node";
import { CameraControllerDevice } from "@matter/node/devices/camera-controller";
import { CommissioningController } from "@project-chip/matter.js";
import { Readable } from "node:stream";
import { WebRtcTransportRequestorServer } from "./behaviors/WebRtcTransportRequestorServer.js";
import { BorderRouterDiscovery } from "./BorderRouterDiscovery.js";
import { ControllerCommandHandler } from "./ControllerCommandHandler.js";
import { LegacyDataInjector } from "./LegacyDataInjector.js";
import { resolveServerId } from "./ServerIdResolver.js";
const logger = Logger.get("MatterController");
let bleSupportLoaded;
async function loadBleSupport(environment, bleProxyEnabled) {
  if (!environment.vars.get("ble.enable", false)) return;
  if (bleProxyEnabled) return;
  if (bleSupportLoaded === void 0) {
    bleSupportLoaded = (async () => {
      try {
        await import("@matter/nodejs-ble");
      } catch (error) {
        logger.error(
          `Failed to load '@matter/nodejs-ble'. Disable BLE or ensure the package is installed.`,
          error
        );
        throw error;
      }
    })();
  }
  return bleSupportLoaded;
}
async function computeCompressedNodeId(crypto, fabricId, caKey) {
  return (await GlobalFabricId.compute(crypto, FabricId(fabricId), caKey)).toString();
}
function parseVersionToNumber(version) {
  const baseVersion = version.split("-")[0];
  const parts = baseVersion.split(".");
  const major = parseInt(parts[0] ?? "0", 10);
  const minor = parseInt(parts[1] ?? "0", 10);
  const patch = parseInt(parts[2] ?? "0", 10);
  return major * 1e4 + minor * 100 + patch;
}
class MatterController {
  #env;
  #controllerInstance;
  #commandHandler;
  #config;
  #serverId;
  #serverVersion;
  #legacyCommissionedDates;
  #enableTestNetDcl = false;
  #disableOtaProvider = true;
  #disableDclSeed = false;
  #bleProxyEnabled = false;
  #borderRouterDiscovery;
  #webRtcRequestor;
  #services;
  static async create(environment, config, options, legacyData) {
    await loadBleSupport(environment, options.bleProxyEnabled ?? false);
    const serverId = await resolveServerId(
      environment,
      config,
      options,
      legacyData?.vendorId,
      legacyData?.fabricId
    );
    const instance = new MatterController(environment, config, options, serverId);
    const commissionedDates = /* @__PURE__ */ new Map();
    if (legacyData !== void 0) {
      const crypto = environment.get(Crypto);
      const baseStorage = await config.service.open(serverId);
      try {
        if (legacyData.credentials && legacyData.fabricId) {
          await LegacyDataInjector.injectCredentials(
            baseStorage.createContext("credentials"),
            baseStorage.createContext("fabrics"),
            crypto,
            legacyData.credentials,
            legacyData.fabric
          );
        }
        if (await LegacyDataInjector.injectNodeData(
          baseStorage,
          legacyData.nodeData,
          legacyData.fabric?.fabricIndex
        ) && legacyData.nodeData !== void 0) {
          for (const [nodeIdStr, data] of Object.entries(legacyData.nodeData.nodes)) {
            const { date_commissioned: commissionedAt } = data;
            commissionedDates.set(nodeIdStr, Timestamp(new Date(commissionedAt).getTime()));
          }
        }
        const lastNodeId = legacyData.nodeData?.last_node_id;
        if (typeof lastNodeId === "number" || typeof lastNodeId === "bigint") {
          if (BigInt(config.nextNodeId) <= BigInt(lastNodeId)) {
            const newNextNodeId = BigInt(lastNodeId) + 10n;
            logger.info(
              `Updating nextNodeId from ${config.nextNodeId} to ${newNextNodeId} (legacy last_node_id: ${lastNodeId})`
            );
            await config.set({ nextNodeId: newNextNodeId });
          }
        }
      } finally {
        await baseStorage.close();
      }
    }
    await instance.initialize(legacyData?.vendorId, legacyData?.fabricId, commissionedDates);
    return instance;
  }
  constructor(environment, config, options, serverId) {
    this.#env = environment;
    this.#borderRouterDiscovery = new BorderRouterDiscovery(this.#env);
    this.#config = config;
    this.#serverId = serverId;
    this.#serverVersion = options.serverVersion ?? "0.0.0";
    this.#enableTestNetDcl = options.enableTestNetDcl ?? this.#enableTestNetDcl;
    this.#disableOtaProvider = options.disableOtaProvider ?? this.#disableOtaProvider;
    this.#disableDclSeed = options.disableDclSeed ?? this.#disableDclSeed;
    this.#bleProxyEnabled = options.bleProxyEnabled ?? this.#bleProxyEnabled;
    this.#services = this.#env.asDependent();
  }
  async initialize(vendorId, fabricId, legacyCommissionedDates) {
    this.#legacyCommissionedDates = legacyCommissionedDates?.size ? legacyCommissionedDates : void 0;
    const trustTestCertificates = this.#enableTestNetDcl;
    if (this.#disableDclSeed) {
      new DclCertificateService(this.#env.root, {
        fetchTestCertificates: true,
        acceptTestCertificates: trustTestCertificates
      });
    } else {
      new DclCertificateService(this.#env.root, {
        fetchTestCertificates: true,
        acceptTestCertificates: trustTestCertificates,
        seed: {
          paaRoots: paaRoots({ includeTest: true }),
          cdSigners: cdSigners({ includeTest: true })
        }
      });
      new DclVendorInfoService(this.#env.root, {
        seed: { vendors: vendors({ includeTest: trustTestCertificates }) }
      });
      this.#services.get(DclVendorInfoService);
    }
    this.#services.get(DclCertificateService);
    this.#controllerInstance = new CommissioningController({
      environment: {
        environment: this.#env,
        id: this.#serverId
      },
      autoConnect: false,
      // Do not auto-connect to the commissioned nodes
      adminFabricLabel: this.#config.fabricLabel,
      adminVendorId: vendorId !== void 0 ? VendorId(vendorId) : void 0,
      adminFabricId: fabricId !== void 0 ? FabricId(fabricId) : void 0,
      rootNodeId: NodeId(112233),
      // TODO Remove when we switch to random IDs
      enableOtaProvider: !this.#disableOtaProvider,
      tcp: true,
      transportPreference: "tcp",
      basicInformation: {
        vendorName: "Open Home Foundation",
        productName: "OHF Matter Server",
        productId: 1,
        hardwareVersion: 1,
        hardwareVersionString: "1.0",
        softwareVersion: parseVersionToNumber(this.#serverVersion) || 1,
        softwareVersionString: this.#serverVersion.split("-")[0]
        // Base version without alpha/beta suffix
      }
    });
  }
  get commandHandler() {
    if (this.#controllerInstance === void 0) {
      throw new Error("Controller not initialized");
    }
    if (this.#commandHandler === void 0) {
      this.#commandHandler = new ControllerCommandHandler(
        this.#controllerInstance,
        this.#env.vars.get("ble.enable", false),
        this.#bleProxyEnabled,
        !this.#disableOtaProvider
      );
      this.#commandHandler.events.started.once(async () => {
        this.#controllerInstance.node.behaviors.require(DclBehavior);
        await this.#controllerInstance.node.setStateOf(DclBehavior, {
          fetchTestCertificates: true,
          acceptTestCertificates: this.#enableTestNetDcl
        });
        const initPromises = new Array();
        if (this.#legacyCommissionedDates !== void 0) {
          initPromises.push(this.injectCommissionedDates());
        }
        initPromises.push(this.vendorInfoService());
        if (!this.#disableOtaProvider && this.#enableTestNetDcl) {
          initPromises.push(this.#enableTestOtaImages());
        }
        initPromises.push(this.#borderRouterDiscovery.start());
        initPromises.push(this.#enableWebRtcRequestor());
        try {
          await MatterAggregateError.allSettled(initPromises);
        } catch (error) {
          logger.error("Error initializing controller additional services", error);
        }
      });
    }
    return this.#commandHandler;
  }
  get borderRouters() {
    return this.#borderRouterDiscovery;
  }
  get webRtcRequestor() {
    if (!this.#webRtcRequestor) {
      throw new Error("WebRTC requestor endpoint not initialized");
    }
    return this.#webRtcRequestor;
  }
  async #enableWebRtcRequestor() {
    if (!this.#controllerInstance) {
      throw new Error("Controller not started");
    }
    const node = this.#controllerInstance.node;
    if (node.endpoints.has("camera-controller")) {
      this.#webRtcRequestor = node.endpoints.for("camera-controller");
      return;
    }
    this.#webRtcRequestor = await node.add(
      new Endpoint(CameraControllerDevice.with(WebRtcTransportRequestorServer), { id: "camera-controller" })
    );
  }
  /**
   * Get the DCL vendor info service instance.
   * Lazily initializes the service if not already present.
   */
  async vendorInfoService() {
    if (this.#controllerInstance === void 0) {
      throw new Error("Controller not initialized");
    }
    const service = await this.#controllerInstance.node.act((agent) => agent.get(DclBehavior).vendorInfoService);
    await service.construction;
    return service;
  }
  /**
   * Get the DCL certificate service instance
   * Lazily initializes the service if not already present.
   */
  async certificateService() {
    if (this.#controllerInstance === void 0) {
      throw new Error("Controller not initialized");
    }
    const service = await this.#controllerInstance.node.act((agent) => agent.get(DclBehavior).certificateService);
    await service.construction;
    return service;
  }
  /**
   * Get the DCL OTA update service instance
   * Lazily initializes the service if not already present.
   */
  async otaUpdateService() {
    if (this.#controllerInstance === void 0) {
      throw new Error("Controller not initialized");
    }
    const service = await this.#controllerInstance.node.act((agent) => agent.get(DclBehavior).otaUpdateService);
    await service.construction;
    return service;
  }
  /**
   * Get vendor information by vendor ID.
   * Returns undefined if the vendor is not found.
   */
  async getVendorInfo(vendorId) {
    return (await this.vendorInfoService()).infoFor(vendorId);
  }
  /**
   * Get all vendor information from the DCL service.
   */
  async getAllVendors() {
    return (await this.vendorInfoService()).vendors;
  }
  async injectCommissionedDates() {
    if (this.#controllerInstance === void 0 || this.#legacyCommissionedDates === void 0) {
      return;
    }
    for (const [nodeIdStr, commissionedAt] of this.#legacyCommissionedDates) {
      try {
        const peerAddress = this.#controllerInstance.fabric.addressOf(NodeId(BigInt(nodeIdStr)));
        const node = await this.#controllerInstance.node.peers.forAddress(peerAddress);
        const commissioningState = node.maybeStateOf(CommissioningClient);
        if (commissioningState !== void 0 && commissioningState.commissionedAt === void 0) {
          await node.setStateOf(CommissioningClient, { commissionedAt });
        }
      } catch (error) {
        logger.warn(`Error injecting commissioned date for node ${nodeIdStr}`, error);
      }
    }
  }
  async stop() {
    await this.#borderRouterDiscovery.stop();
    await this.#commandHandler?.close();
    await this.#services.close();
  }
  /**
   * Enable test OTA images (test-net DCL).
   * Must be called after the controller is started.
   */
  async #enableTestOtaImages() {
    if (this.#controllerInstance === void 0) {
      throw new Error("Controller not initialized");
    }
    await this.#controllerInstance.otaProvider.setStateOf(SoftwareUpdateManager, {
      allowTestOtaImages: true
    });
    logger.info("Enabled test OTA images (test-net DCL)");
  }
  /**
   * Store an OTA image file from a file path.
   * @param filePath - Path to the OTA file
   * @returns true if stored successfully
   */
  async storeOtaImageFromFile(filePath) {
    const { createReadStream } = await import("node:fs");
    const { pathToFileURL } = await import("node:url");
    const otaService = await this.otaUpdateService();
    const fileUrl = pathToFileURL(filePath).href;
    const infoStream = Readable.toWeb(createReadStream(filePath));
    const updateInfo = await otaService.updateInfoFromStream(infoStream, fileUrl);
    logger.info(
      `Storing OTA image from ${filePath}: vendorId=0x${updateInfo.vid.toString(16)}, productId=0x${updateInfo.pid.toString(16)}, version=${updateInfo.softwareVersion} (${updateInfo.softwareVersionString})`
    );
    const storeStream = Readable.toWeb(createReadStream(filePath));
    await otaService.store(storeStream, updateInfo, "local");
    return true;
  }
}
export {
  MatterController,
  computeCompressedNodeId
};
//# sourceMappingURL=MatterController.js.map
