/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  Construction,
  Days,
  Diagnostic,
  Duration,
  Logger,
  StorageService,
  Time,
  asError
} from "@matter/general";
import {
  DiscoveryCapabilitiesSchema,
  EnhancedSetupFlowOptionsSchema
} from "@matter/types";
import { PairingHintBitmapSchema } from "../advertisement/PairingHintBitmap.js";
import { DclClient, MatterDclError, MatterDclResponseError } from "./DclClient.js";
import { DclConfig } from "./DclConfig.js";
import { DclErrorCodes } from "./DclRestApiTypes.js";
const logger = Logger.get("DclVendorInfoService");
class DclVendorInfoService {
  #construction;
  #storageManager;
  #storage;
  #vendorIndex = /* @__PURE__ */ new Map();
  #updateTimer;
  #closed = false;
  #options;
  #fetchPromise;
  constructor(environment, options = {}) {
    environment.root.set(DclVendorInfoService, this);
    this.#options = options;
    logger.info(
      "Initialize VendorInfoService",
      Diagnostic.dict({
        source: options.dclConfig?.url ?? DclConfig.production.url,
        interval: options.updateInterval === null ? "disabled" : Duration.format(options.updateInterval ?? Days.one)
      })
    );
    this.#construction = Construction(this, async () => {
      this.#storageManager = await environment.get(StorageService).open("vendors");
      this.#storage = this.#storageManager.createContext("info");
      await this.#loadVendors(this.#storage);
      if (options.seed?.vendors) {
        await this.#consumeVendorSeed(options.seed.vendors);
      }
      await this.update();
      if (options.updateInterval !== null) {
        const updateInterval = options.updateInterval ?? Days.one;
        this.#updateTimer = Time.getPeriodicTimer(
          "DCL Vendor Info Update",
          updateInterval,
          () => this.update()
        ).start();
      }
    });
  }
  get construction() {
    return this.#construction;
  }
  /**
   * Get vendor information by vendor ID. Returns undefined if not found.
   */
  infoFor(vendorId) {
    this.construction.assert();
    return this.#vendorIndex.get(vendorId);
  }
  /**
   * Get all vendor information as a map indexed by vendor ID.
   */
  get vendors() {
    this.construction.assert();
    return this.#vendorIndex;
  }
  /**
   * Update vendor information from DCL.
   */
  async update() {
    if (this.#closed || !this.#storage) {
      return;
    }
    if (this.#fetchPromise !== void 0) {
      await this.#fetchPromise;
      return;
    }
    try {
      this.#fetchPromise = this.#fetchVendorsFromDcl(this.#storage).finally(() => {
        this.#fetchPromise = void 0;
      });
      await this.#fetchPromise;
    } catch (error) {
      logger.info("Error updating vendor information", Diagnostic.errorMessage(asError(error)));
    }
  }
  /**
   * Fetch product details for a given vendor ID and product ID from DCL.
   * Returns undefined if the product is not found or the service is closed.
   */
  async productInfoFor(vendorId, productId) {
    if (this.#closed) {
      return void 0;
    }
    try {
      const dclClient = new DclClient(this.#options.dclConfig ?? DclConfig.production);
      const model = await dclClient.fetchModelByVidPid(vendorId, productId, this.#options);
      return this.#normalizeProductModel(model);
    } catch (error) {
      if (error instanceof MatterDclResponseError && error.response.code === DclErrorCodes.NotFound) {
        logger.debug("Product not found in DCL", Diagnostic.dict({ vendorId, productId }));
        return void 0;
      }
      throw error;
    }
  }
  #normalizeProductModel(model) {
    const {
      deviceTypeID,
      productName,
      productLabel,
      partNumber,
      discoveryCapabilitiesBitmask,
      commissioningCustomFlow,
      commissioningCustomFlowUrl,
      commissioningModeInitialStepsHint,
      commissioningModeInitialStepsInstruction,
      commissioningModeSecondaryStepsHint,
      commissioningModeSecondaryStepsInstruction,
      commissioningFallbackUrl,
      userManualUrl,
      supportUrl,
      productUrl,
      lsfUrl,
      lsfRevision,
      enhancedSetupFlowOptions,
      enhancedSetupFlowTCUrl,
      enhancedSetupFlowTCRevision,
      enhancedSetupFlowMaintenanceUrl
    } = model;
    try {
      return {
        deviceTypeID,
        productName,
        productLabel,
        partNumber,
        discoveryCapabilities: DiscoveryCapabilitiesSchema.decode(discoveryCapabilitiesBitmask),
        commissioningFlow: commissioningCustomFlow,
        commissioningCustomFlowUrl,
        commissioningModeInitialStepsHint: PairingHintBitmapSchema.decode(commissioningModeInitialStepsHint),
        commissioningModeInitialStepsInstruction,
        commissioningModeSecondaryStepsHint: PairingHintBitmapSchema.decode(
          commissioningModeSecondaryStepsHint
        ),
        commissioningModeSecondaryStepsInstruction,
        commissioningFallbackUrl,
        userManualUrl,
        supportUrl,
        productUrl,
        lsfUrl,
        lsfRevision,
        enhancedSetupFlowOptions: enhancedSetupFlowOptions !== void 0 ? EnhancedSetupFlowOptionsSchema.decode(enhancedSetupFlowOptions) : void 0,
        enhancedSetupFlowTCUrl,
        enhancedSetupFlowTCRevision,
        enhancedSetupFlowMaintenanceUrl
      };
    } catch (error) {
      logger.warn(
        "Invalid DCL product entry",
        Diagnostic.dict({ vendorId: model.vid, productId: model.pid }),
        error
      );
      return void 0;
    }
  }
  /**
   * Load vendor information from storage.
   */
  async #loadVendors(storage) {
    const storedVendors = await storage.get("vendors", []);
    for (const vendor of storedVendors) {
      this.#vendorIndex.set(vendor.vendorId, vendor);
    }
    if (storedVendors.length > 0) {
      logger.info(`Loaded ${this.#vendorIndex.size} vendors from storage`);
    }
  }
  /**
   * Fetch vendor information from DCL and store it.
   */
  async #fetchVendorsFromDcl(storage) {
    logger.info("Fetching vendor information from DCL");
    const dclClient = new DclClient(this.#options.dclConfig ?? DclConfig.production);
    const vendors = await dclClient.fetchAllVendors(this.#options);
    logger.info(`Fetched ${vendors.length} vendors from DCL`);
    const hardcodedVendors = [
      ...[65521, 65522, 65523, 65524].map((vendorID) => ({
        vendorID,
        vendorName: "Test Vendor",
        companyLegalName: "Test Vendor Inc.",
        companyPreferredName: "Test Vendor",
        vendorLandingPageURL: "https://test.example.com",
        creator: "System"
      })),
      {
        vendorID: 4939,
        vendorName: "Nabu Casa",
        companyLegalName: "Nabu Casa, Inc.",
        companyPreferredName: "Nabu Casa",
        vendorLandingPageURL: "https://www.nabucasa.com",
        creator: "System"
      }
    ];
    for (const hardcoded of hardcodedVendors) {
      if (!vendors.find((v) => v.vendorID === hardcoded.vendorID)) {
        vendors.push(hardcoded);
      }
    }
    const vendorsToStore = [];
    for (const vendor of vendors) {
      const normalizedVendor = this.#normalizeDclVendor(vendor);
      this.#vendorIndex.set(normalizedVendor.vendorId, normalizedVendor);
      vendorsToStore.push(normalizedVendor);
    }
    await storage.set("vendors", vendorsToStore);
    logger.info(`Stored ${this.#vendorIndex.size} vendors`);
  }
  /**
   * Normalize DCL vendor format to internal format.
   */
  #normalizeDclVendor(dclVendor) {
    return {
      vendorId: dclVendor.vendorID,
      vendorName: dclVendor.vendorName,
      companyLegalName: dclVendor.companyLegalName,
      companyPreferredName: dclVendor.companyPreferredName,
      vendorLandingPageUrl: dclVendor.vendorLandingPageURL,
      creator: dclVendor.creator
    };
  }
  async #consumeVendorSeed(source) {
    if (this.#vendorIndex.size >= source.expectedCount) {
      logger.info(
        `seed: skipping vendors \u2014 storage has ${this.#vendorIndex.size} entries >= seed expectedCount ${source.expectedCount}`
      );
      return;
    }
    const collected = new Array();
    try {
      for await (const entry of source.entries) {
        if (this.#closed) break;
        try {
          if (typeof entry.vendorId !== "number" || typeof entry.vendorName !== "string" || !entry.vendorName || entry.kind !== "production" && entry.kind !== "test") {
            throw new MatterDclError("invalid vendor entry shape");
          }
          collected.push({
            vendorId: entry.vendorId,
            vendorName: entry.vendorName,
            companyLegalName: entry.companyLegalName ?? "",
            companyPreferredName: entry.companyPreferredName ?? "",
            vendorLandingPageUrl: entry.vendorLandingPageURL ?? "",
            creator: entry.creator ?? ""
          });
        } catch (err) {
          logger.warn("seed: malformed vendor entry, aborting stream", Diagnostic.errorMessage(asError(err)));
          break;
        }
      }
    } catch (err) {
      logger.warn("seed: vendor stream failed", Diagnostic.errorMessage(asError(err)));
      return;
    }
    if (this.#closed || collected.length === 0) return;
    this.#vendorIndex.clear();
    for (const vendor of collected) {
      this.#vendorIndex.set(vendor.vendorId, vendor);
    }
    await this.#storage.set("vendors", collected);
    logger.info(`seed: consumed ${collected.length} vendors from snapshot ${source.builtAt}`);
  }
  /**
   * Close the service and stop periodic updates.
   */
  async close() {
    if (this.#closed) return;
    this.#closed = true;
    await this.#construction.close(async () => {
      await this.#fetchPromise;
      this.#updateTimer?.stop();
      this.#updateTimer = void 0;
      await this.#storageManager?.close();
    });
  }
}
export {
  DclVendorInfoService
};
//# sourceMappingURL=DclVendorInfoService.js.map
