/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { PersistedFileDesignator } from "#bdx/PersistedFileDesignator.js";
import { ScopedStorage } from "#bdx/ScopedStorage.js";
import { DclErrorCodes } from "#dcl/DclRestApiTypes.js";
import {
  asError,
  Construction,
  Crypto,
  Diagnostic,
  hashAlgorithmForId,
  ImplementationError,
  Logger,
  MatterError,
  Minutes,
  StorageService
} from "@matter/general";
import { VendorId } from "@matter/types";
import { OtaImageReader } from "../ota/OtaImageReader.js";
import { DclClient, MatterDclError, MatterDclResponseError } from "./DclClient.js";
import { DclConfig } from "./DclConfig.js";
const logger = Logger.get("DclOtaUpdateService");
class OtaUpdateError extends MatterError {
  constructor(message, options) {
    super(message, options);
  }
}
const OTA_DOWNLOAD_TIMEOUT = Minutes(5);
const OTA_FILENAME_REGEX = /^[0-9a-f]+[./][0-9a-f]+[./](?:prod|test|local)[./]\d+$/i;
class DclOtaUpdateService {
  #construction;
  #crypto;
  #options;
  #blobDriver;
  #closeBlobStorage;
  #storage;
  #closed = false;
  get construction() {
    return this.#construction;
  }
  constructor(environment, options) {
    environment.root.set(DclOtaUpdateService, this);
    this.#crypto = environment.get(Crypto);
    this.#options = options;
    logger.info(
      "Initialize OTAUpdateService",
      Diagnostic.dict({
        prod: options?.productionDclConfig?.url ?? DclConfig.production.url,
        test: options?.testDclConfig?.url ?? DclConfig.test.url
      })
    );
    this.#construction = Construction(this, async () => {
      const blobHandle = await environment.get(StorageService).openBlobStorage("ota");
      this.#blobDriver = blobHandle.driver;
      this.#closeBlobStorage = blobHandle.close;
      this.#storage = new ScopedStorage(this.#blobDriver, ["bin"], "ota");
      await this.#migrateStorage();
    });
  }
  get storage() {
    this.construction.assert();
    return this.#storage;
  }
  async close() {
    this.#closed = true;
    await this.#construction.close(async () => {
      await this.#closeBlobStorage?.();
    });
  }
  async #migrateStorage() {
    const blobDriver = this.#blobDriver;
    const baseContexts = this.#storage.baseContexts;
    for (const vendorHex of await blobDriver.contexts(baseContexts)) {
      const vendorContexts = [...baseContexts, vendorHex];
      for (const productHex of await blobDriver.contexts(vendorContexts)) {
        const productContexts = [...vendorContexts, productHex];
        const keys = await blobDriver.keys(productContexts);
        for (const key of keys) {
          if (key !== "prod" && key !== "test") {
            continue;
          }
          try {
            const headerBlob = await blobDriver.openBlob(productContexts, key);
            const headerReader = headerBlob.stream().getReader();
            const header = await OtaImageReader.header(headerReader);
            await headerReader.cancel();
            const versionKey = header.softwareVersion.toString();
            const modeContexts = [...productContexts, key];
            const copyBlob = await blobDriver.openBlob(productContexts, key);
            await blobDriver.writeBlobFromStream(modeContexts, versionKey, copyBlob.stream());
            await blobDriver.delete(productContexts, key);
            logger.info(
              `Migrated OTA storage: ${vendorHex}.${productHex}.${key} -> ${vendorHex}.${productHex}.${key}.${versionKey}`
            );
          } catch (error) {
            logger.warn(
              `Failed to migrate OTA file ${vendorHex}.${productHex}.${key}, deleting corrupt entry:`,
              error
            );
            try {
              await blobDriver.delete(productContexts, key);
            } catch {
            }
          }
        }
      }
    }
  }
  async #queryDclForUpdate(options) {
    const { vendorId, productId, currentSoftwareVersion, isProduction, targetSoftwareVersion } = options;
    const config = isProduction ? this.#options?.productionDclConfig ?? DclConfig.production : this.#options?.testDclConfig ?? DclConfig.test;
    const dclClient = new DclClient(config);
    const dclLogStr = isProduction ? "Prod-DCL" : "Test-DCL";
    const diagnosticInfo = {
      vid: vendorId,
      pid: productId,
      prod: isProduction,
      current: currentSoftwareVersion
    };
    try {
      if (targetSoftwareVersion !== void 0) {
        logger.debug(
          `Checking update in ${dclLogStr} for specific version`,
          Diagnostic.dict({
            ...diagnosticInfo,
            target: targetSoftwareVersion
          })
        );
        return await this.#checkSpecificVersion(
          dclClient,
          vendorId,
          productId,
          targetSoftwareVersion,
          currentSoftwareVersion
        );
      }
      const softwareVersions = await dclClient.fetchModelVersionsByVidPid(vendorId, productId);
      const newerVersions = softwareVersions.filter((version) => version > currentSoftwareVersion).sort((a, b) => b - a);
      if (newerVersions.length === 0) {
        logger.debug(`No newer versions available in ${dclLogStr}`, Diagnostic.dict(diagnosticInfo));
        return;
      }
      logger.debug(
        `Found ${newerVersions.length} newer version(s) in ${dclLogStr}`,
        Diagnostic.dict(diagnosticInfo)
      );
      for (const version of newerVersions) {
        if (this.#closed) {
          return;
        }
        try {
          const updateInfo = await this.#checkSpecificVersion(
            dclClient,
            vendorId,
            productId,
            version,
            currentSoftwareVersion
          );
          if (updateInfo !== void 0) {
            logger.info(
              `Update available in ${dclLogStr}`,
              Diagnostic.dict({ new: version, ...diagnosticInfo })
            );
            return updateInfo;
          }
        } catch (error) {
          MatterDclError.accept(error);
          logger.info(`Failed to check for update for VID: ${vendorId}, PID: ${productId}: ${error.message}`);
        }
      }
      logger.debug(`No applicable updates found in ${dclLogStr}`, Diagnostic.dict(diagnosticInfo));
    } catch (error) {
      MatterDclError.accept(error);
      if (error instanceof MatterDclResponseError && error.response.code === DclErrorCodes.NotFound) {
        logger.debug(`No applicable updates found in ${dclLogStr}`, Diagnostic.dict(diagnosticInfo));
      } else {
        logger.info(`Failed to check for updates for VID: ${vendorId}, PID: ${productId}: ${error.message}`);
      }
    }
  }
  /**
   * Check DCL for available software updates for a device, defined by its vendor ID, product ID, and current
   * software version. If a target software version is provided, only that version is checked for applicability and
   * ignoring other newer versions.
   * If isProduction flag is specified, it is exactly used to find updates. Leave that flag undefined to find both
   * test and production updates.
   */
  async checkForUpdate(options) {
    const {
      vendorId,
      productId,
      currentSoftwareVersion,
      includeStoredUpdates = false,
      isProduction,
      targetSoftwareVersion
    } = options;
    const foundUpdates = new Array();
    if (includeStoredUpdates) {
      const localUpdates = await this.find({
        vendorId,
        productId,
        currentVersion: currentSoftwareVersion
      });
      for (let i = localUpdates.length - 1; i >= 0; i--) {
        const entry = localUpdates[i];
        if (isProduction === true && entry.mode === "test") continue;
        const localUpdate = {
          ...entry,
          vid: VendorId(vendorId),
          pid: productId,
          cdVersionNumber: 0,
          softwareVersionValid: true,
          schemaVersion: 1,
          minApplicableSoftwareVersion: entry.minApplicableSoftwareVersion ?? 0,
          maxApplicableSoftwareVersion: entry.maxApplicableSoftwareVersion ?? entry.softwareVersion - 1,
          source: entry.mode === "prod" ? "dcl-prod" : entry.mode === "test" ? "dcl-test" : "local"
        };
        if (localUpdate.softwareVersion > currentSoftwareVersion && currentSoftwareVersion >= localUpdate.minApplicableSoftwareVersion && currentSoftwareVersion <= localUpdate.maxApplicableSoftwareVersion) {
          logger.debug(`Found applicable local update`, Diagnostic.dict(localUpdate));
          if (targetSoftwareVersion !== void 0 && localUpdate.softwareVersion === targetSoftwareVersion) {
            return localUpdate;
          }
          foundUpdates.push(localUpdate);
        }
      }
    }
    if (isProduction !== false && !this.#closed) {
      const prodUpdate = await this.#queryDclForUpdate({ ...options, isProduction: true });
      if (prodUpdate !== void 0) {
        const updateEntry = {
          ...prodUpdate,
          source: "dcl-prod"
        };
        if (targetSoftwareVersion !== void 0 && updateEntry.softwareVersion === targetSoftwareVersion) {
          return updateEntry;
        }
        foundUpdates.push(updateEntry);
      }
    }
    if (isProduction !== true && !this.#closed) {
      const testUpdate = await this.#queryDclForUpdate({ ...options, isProduction: false });
      if (testUpdate !== void 0) {
        const updateEntry = {
          ...testUpdate,
          source: "dcl-test"
        };
        if (targetSoftwareVersion !== void 0 && updateEntry.softwareVersion === targetSoftwareVersion) {
          return updateEntry;
        }
        foundUpdates.push(updateEntry);
      }
    }
    if (targetSoftwareVersion !== void 0 || !foundUpdates.length) {
      return;
    }
    foundUpdates.sort((a, b) => b.softwareVersion - a.softwareVersion);
    return foundUpdates[0];
  }
  async #verifyUpdate(updateInfo, fileDesignator) {
    const { otaFileSize, softwareVersion, vid, pid } = updateInfo;
    const storedBlob = await fileDesignator.openBlob();
    const reader = storedBlob.stream().getReader();
    let checksumOptions;
    if (updateInfo.otaChecksum) {
      const checksumType = hashAlgorithmForId(updateInfo.otaChecksumType ?? 1);
      if (checksumType === void 0) {
        throw new OtaUpdateError(`Unsupported OTA checksum type: ${updateInfo.otaChecksumType}`);
      }
      checksumOptions = {
        calculateFullChecksum: true,
        checksumType,
        expectedChecksum: updateInfo.otaChecksum
      };
    }
    const header = await OtaImageReader.file(reader, this.#crypto, otaFileSize, checksumOptions);
    if (header.vendorId !== vid) {
      throw new OtaUpdateError(`OTA image vendor ID mismatch: expected ${vid}, got ${header.vendorId}`);
    }
    if (header.productId !== pid) {
      throw new OtaUpdateError(`OTA image product ID mismatch: expected ${pid}, got ${header.productId}`);
    }
    if (header.softwareVersion !== softwareVersion) {
      throw new OtaUpdateError(
        `OTA image software version mismatch: expected ${softwareVersion}, got ${header.softwareVersion}`
      );
    }
  }
  #fileName(vid, pid, mode, softwareVersion) {
    return `${vid.toString(16)}.${pid.toString(16)}.${mode}.${softwareVersion}`;
  }
  /**
   * Store an OTA update image from a ReadableStream into persistent storage.
   */
  async store(stream, updateInfo, mode = true) {
    if (this.#storage === void 0) {
      await this.construction;
    }
    const storage = this.#storage;
    if (typeof mode === "boolean") {
      mode = mode ? "prod" : "test";
    }
    const { softwareVersion, softwareVersionString, vid, pid } = updateInfo;
    const filename = this.#fileName(vid, pid, mode, softwareVersion);
    const fileDesignator = new PersistedFileDesignator(filename, storage);
    const diagnosticInfo = {
      filename,
      vid,
      pid,
      v: `${softwareVersion} (${softwareVersionString})`,
      mode
    };
    try {
      await fileDesignator.writeFromStream(stream);
      await this.#verifyUpdate(updateInfo, fileDesignator);
      logger.debug(`Stored OTA image`, Diagnostic.dict(diagnosticInfo));
      return fileDesignator;
    } catch (error) {
      try {
        await fileDesignator.delete();
      } catch {
      }
      throw error;
    }
  }
  /**
   * Download and validate an OTA update image from a HTTP pr HTTPS server.
   *
   * If a file with the same vendor ID, product ID, and production mode already exists
   * and passes validation, it will be reused instead of downloading again, unless force is true.
   *
   * Production and test files are stored separately to avoid conflicts.
   *
   * Returns a PersistedFileDesignator for the validated OTA image
   */
  async downloadUpdate(updateInfo, force = false, timeout = OTA_DOWNLOAD_TIMEOUT) {
    if (this.#storage === void 0) {
      await this.construction;
    }
    const storage = this.#storage;
    const { otaUrl, softwareVersion, softwareVersionString, vid, pid, source } = updateInfo;
    const mode = source === "dcl-prod" ? "prod" : source === "dcl-test" ? "test" : "local";
    const diagnosticInfo = {
      vid,
      pid,
      v: `${softwareVersion} (${softwareVersionString})`,
      mode
    };
    const filename = this.#fileName(vid, pid, mode, softwareVersion);
    const fileDesignator = new PersistedFileDesignator(filename, storage);
    if (await fileDesignator.exists()) {
      if (!force) {
        try {
          await this.#verifyUpdate(updateInfo, fileDesignator);
          logger.info(`Existing OTA image validated successfully`, Diagnostic.dict(diagnosticInfo));
          return fileDesignator;
        } catch (error) {
          logger.info(
            `Existing OTA image validation failed, Re-downloading ...`,
            Diagnostic.errorMessage(asError(error))
          );
        }
      }
      await fileDesignator.delete();
    }
    if (!otaUrl?.trim()) {
      throw new OtaUpdateError("No OTA URL provided in update info");
    }
    let url;
    try {
      url = new URL(otaUrl);
    } catch {
      throw new OtaUpdateError(`Invalid OTA URL: ${otaUrl}`);
    }
    const protocol = url.protocol.toLowerCase();
    if (protocol !== "https:" && protocol !== "http:") {
      throw new OtaUpdateError(
        `Unsupported protocol "${protocol}" in OTA URL. Only "https:", "http:" are supported`
      );
    }
    logger.info(`Downloading OTA image from ${otaUrl}`, Diagnostic.dict(diagnosticInfo));
    try {
      const response = await fetch(otaUrl, {
        method: "GET",
        signal: AbortSignal.timeout(timeout)
      });
      if (!response.ok) {
        throw new OtaUpdateError(`Failed to download OTA image: ${response.status} ${response.statusText}`);
      }
      if (!response.body) {
        throw new OtaUpdateError("No response body received");
      }
      return await this.store(response.body, updateInfo, mode);
    } catch (error) {
      MatterError.reject(error);
      const otaError = new OtaUpdateError(`Failed to download OTA image from ${otaUrl}`);
      otaError.cause = error;
      throw otaError;
    }
  }
  /**
   * Check a specific software version for applicability.
   *
   * @param dclClient - DCL client to use for fetching
   * @param vendorId - Vendor ID of the device
   * @param productId - Product ID of the device
   * @param softwareVersion - The specific version to check
   * @param currentVersion - Current software version of the device
   * @returns Update information if the version is applicable, undefined otherwise
   */
  async #checkSpecificVersion(dclClient, vendorId, productId, softwareVersion, currentVersion) {
    const versionInfo = await dclClient.fetchModelVersionByVidPidSoftwareVersion(
      vendorId,
      productId,
      softwareVersion
    );
    if (versionInfo.softwareVersion !== softwareVersion) {
      throw new OtaUpdateError(
        `Version mismatch: requested ${softwareVersion}, but DCL returned ${versionInfo.softwareVersion}`
      );
    }
    if (this.#isVersionApplicable(versionInfo, currentVersion)) {
      logger.debug(
        `Found applicable update: version ${softwareVersion} (${versionInfo.softwareVersionString}) for current version ${currentVersion}`
      );
      return versionInfo;
    }
    logger.debug(
      `Version ${softwareVersion} is not applicable`,
      Diagnostic.dict({
        valid: versionInfo.softwareVersionValid,
        url: !!versionInfo.otaUrl,
        min: versionInfo.minApplicableSoftwareVersion,
        max: versionInfo.maxApplicableSoftwareVersion
      })
    );
  }
  /**
   * Check if a software version is applicable for the current device version.
   */
  #isVersionApplicable(versionInfo, currentVersion) {
    if (!versionInfo.softwareVersionValid) {
      return false;
    }
    if (!versionInfo.otaUrl?.trim()) {
      return false;
    }
    if (!versionInfo.otaUrl.toLowerCase().startsWith("https://")) {
      logger.warn(`Invalid OTA URL from DCL: "${versionInfo.otaUrl}" - must use https:// protocol`);
      return false;
    }
    if (versionInfo.minApplicableSoftwareVersion !== void 0 && currentVersion < versionInfo.minApplicableSoftwareVersion) {
      return false;
    }
    if (versionInfo.maxApplicableSoftwareVersion !== void 0 && currentVersion > versionInfo.maxApplicableSoftwareVersion) {
      return false;
    }
    return true;
  }
  /**
   * Create OtaUpdateInfo from an OTA image stream.
   *
   * This method reads and validates an OTA image from a ReadableStream and creates an OtaUpdateInfo
   * structure. This is useful for providing OTA updates from local files or any stream source.
   *
   * The OTA image header contains most required information. However, some DCL-specific
   * fields are not present in the OTA header and must be provided via options or use defaults:
   * - cdVersionNumber: Certificate Declaration version (defaults to 1)
   * - softwareVersionValid: Validity flag (defaults to true)
   * - schemaVersion: DCL schema version (defaults to 0)
   * - minApplicableSoftwareVersion: If not in header, defaults to 0 (all versions)
   * - maxApplicableSoftwareVersion: If not in header, defaults to current version minus 1
   *
   * @param stream - ReadableStream of the OTA image
   * @param otaUrl - URL to use for the OTA file (should be file:// for local files or https:// for remote)
   * @param options - Optional parameters for DCL-specific fields not in OTA header
   * @returns OtaUpdateInfo with stream data and header information
   */
  async updateInfoFromStream(stream, otaUrl, options) {
    const reader = stream.getReader();
    const header = await OtaImageReader.header(reader);
    const updateInfo = {
      vid: VendorId(header.vendorId),
      pid: header.productId,
      softwareVersion: header.softwareVersion,
      softwareVersionString: header.softwareVersionString,
      cdVersionNumber: options?.cdVersionNumber ?? 1,
      softwareVersionValid: options?.softwareVersionValid ?? true,
      otaUrl,
      // Use header values if present, otherwise use provided options or defaults
      minApplicableSoftwareVersion: header.minApplicableSoftwareVersion ?? options?.minApplicableSoftwareVersion ?? 0,
      maxApplicableSoftwareVersion: header.maxApplicableSoftwareVersion ?? options?.maxApplicableSoftwareVersion ?? header.softwareVersion - 1,
      releaseNotesUrl: header.releaseNotesUrl,
      schemaVersion: options?.schemaVersion ?? 0,
      source: "local"
    };
    logger.debug(
      `OTA update info from stream`,
      Diagnostic.dict({
        vid: header.vendorId,
        pid: header.productId,
        v: `${header.softwareVersion} (${header.softwareVersionString})`
      })
    );
    return updateInfo;
  }
  /**
   * Create OtaUpdateInfo from a remote OTA image file URL.
   *
   * This method fetches and validates a remote OTA image file (via HTTPS) and creates an OtaUpdateInfo
   * structure that can be used with downloadUpdate(). For local files, use updateInfoFromStream() instead.
   *
   * The OTA image header contains most required information. However, some DCL-specific
   * fields are not present in the OTA header and must be provided via options or use defaults:
   * - cdVersionNumber: Certificate Declaration version (defaults to 1)
   * - softwareVersionValid: Validity flag (defaults to true)
   * - schemaVersion: DCL schema version (defaults to 0)
   * - minApplicableSoftwareVersion: If not in header, defaults to 0 (all versions)
   * - maxApplicableSoftwareVersion: If not in header, defaults to current version
   *
   * @param fileUrl - HTTPS URL to the OTA image file
   * @param options - Optional parameters for DCL-specific fields not in OTA header
   * @returns OtaUpdateInfo with file URL and header data
   */
  async createUpdateInfoFromFile(fileUrl, options) {
    const fileProtocol = fileUrl.split(":")[0].toLowerCase();
    if (fileProtocol !== "https" && fileProtocol !== "http") {
      throw new OtaUpdateError(
        `Only HTTP(S) URLs are supported. For local files, use createUpdateInfoFromStream() instead.`
      );
    }
    try {
      const response = await fetch(fileUrl, { method: "GET" });
      if (!response.ok) {
        throw new OtaUpdateError(`Failed to fetch OTA image: ${response.status} ${response.statusText}`);
      }
      if (!response.body) {
        throw new OtaUpdateError("No response body received");
      }
      return await this.updateInfoFromStream(response.body, fileUrl, options);
    } catch (error) {
      MatterError.reject(error);
      const otaError = new OtaUpdateError(`Failed to read OTA image from ${fileUrl}`);
      otaError.cause = error;
      throw otaError;
    }
  }
  /**
   * Find downloaded OTA updates in storage.
   *
   * This method scans the OTA storage for downloaded update files and returns
   * information about them. Files can be filtered by vendor ID, product ID,
   * and production/test mode.
   *
   * @param options - Optional filter criteria
   * @param options.vendorId - Filter by vendor ID
   * @param options.productId - Filter by product ID (requires vendorId)
   * @param options.isProduction - Filter by production (true) or test (false) mode
   * @returns Array of downloaded update information
   */
  async find(options = {}) {
    if (this.#storage === void 0) {
      await this.construction;
    }
    const results = await this.#findEntries(this.#storage.baseContexts, options);
    const modeOrder = { prod: 0, test: 1, local: 2 };
    results.sort((a, b) => {
      const modeDiff = (modeOrder[a.mode] ?? 99) - (modeOrder[b.mode] ?? 99);
      if (modeDiff !== 0) return modeDiff;
      if (a.vendorId !== b.vendorId) return a.vendorId - b.vendorId;
      if (a.productId !== b.productId) return a.productId - b.productId;
      return a.softwareVersion - b.softwareVersion;
    });
    return results;
  }
  async #findEntries(baseContexts, options) {
    const blobDriver = this.#blobDriver;
    const { vendorId } = options;
    if (vendorId !== void 0) {
      const vendorContexts = [...baseContexts, vendorId.toString(16)];
      const vendorEntries = await this.#findVendorEntries(vendorContexts, options);
      return vendorEntries.map((entry) => ({
        ...entry,
        vendorId,
        filename: this.#fileName(vendorId, entry.productId, entry.mode, entry.softwareVersion)
      }));
    }
    const result = new Array();
    for (const vendorIdHex of await blobDriver.contexts(baseContexts)) {
      const vendorId2 = parseInt(vendorIdHex, 16);
      const vendorContexts = [...baseContexts, vendorIdHex];
      const vendorEntries = await this.#findVendorEntries(vendorContexts, options);
      result.push(
        ...vendorEntries.map((entry) => ({
          ...entry,
          vendorId: vendorId2,
          filename: this.#fileName(vendorId2, entry.productId, entry.mode, entry.softwareVersion)
        }))
      );
    }
    return result;
  }
  async #findVendorEntries(vendorContexts, options) {
    const blobDriver = this.#blobDriver;
    const { productId } = options;
    if (productId !== void 0) {
      const productContexts = [...vendorContexts, productId.toString(16)];
      const productEntries = await this.#findVendorProductEntries(productContexts, options);
      return productEntries.map((entry) => ({ ...entry, productId }));
    }
    const result = new Array();
    for (const productIdHex of await blobDriver.contexts(vendorContexts)) {
      const productId2 = parseInt(productIdHex, 16);
      const productContexts = [...vendorContexts, productIdHex];
      const productEntries = await this.#findVendorProductEntries(productContexts, options);
      result.push(...productEntries.map((entry) => ({ ...entry, productId: productId2 })));
    }
    return result;
  }
  async #findVendorProductEntries(productContexts, options) {
    const blobDriver = this.#blobDriver;
    const { isProduction, mode: filterMode } = options;
    const result = new Array();
    const modeSubContexts = await blobDriver.contexts(productContexts);
    const validModes = ["prod", "test", "local"];
    for (const modeStr of modeSubContexts) {
      if (!validModes.includes(modeStr)) {
        continue;
      }
      const mode = modeStr;
      if (filterMode !== void 0 && filterMode !== mode) {
        continue;
      }
      if (filterMode === void 0 && isProduction !== void 0) {
        if (isProduction === true && mode !== "prod") continue;
        if (isProduction === false && mode === "prod") continue;
      }
      const modeContexts = [...productContexts, modeStr];
      const versionKeys = await blobDriver.keys(modeContexts);
      for (const versionKey of versionKeys) {
        const fileDesignator = new PersistedFileDesignator(versionKey, blobDriver, modeContexts);
        const entry = await this.#checkEntry(fileDesignator, options);
        if (entry !== void 0) {
          result.push({
            ...entry,
            mode
          });
        }
      }
    }
    return result;
  }
  async #checkEntry(fileDesignator, options) {
    try {
      const blob = await fileDesignator.openBlob();
      const reader = blob.stream().getReader();
      const header = await OtaImageReader.header(reader);
      await reader.cancel();
      const { currentVersion } = options;
      if (currentVersion !== void 0) {
        if (header.minApplicableSoftwareVersion !== void 0 && currentVersion < header.minApplicableSoftwareVersion) {
          return;
        }
        if (header.maxApplicableSoftwareVersion !== void 0 && currentVersion > header.maxApplicableSoftwareVersion) {
          return;
        }
      }
      return {
        softwareVersion: header.softwareVersion,
        softwareVersionString: header.softwareVersionString,
        minApplicableSoftwareVersion: header.minApplicableSoftwareVersion,
        maxApplicableSoftwareVersion: header.maxApplicableSoftwareVersion,
        size: blob.size
      };
    } catch (error) {
      logger.warn(`Failed to read OTA file ${fileDesignator.text}:`, Diagnostic.errorMessage(asError(error)));
    }
  }
  /** Get a PersistedFileDesignator for a stored OTA update file by filename. */
  async fileDesignatorForUpdate(filename) {
    if (!OTA_FILENAME_REGEX.test(filename)) {
      throw new ImplementationError(`Invalid OTA filename format: ${filename}`);
    }
    if (this.#storage === void 0) {
      await this.construction;
    }
    const fileDesignator = new PersistedFileDesignator(filename, this.#storage);
    if (!await fileDesignator.exists()) {
      throw new OtaUpdateError(`OTA file not found: ${filename}`);
    }
    return fileDesignator;
  }
  /**
   * Delete stored OTA update files.
   *
   * Can delete by specific filename or by vendor ID, product ID, and mode.
   * If only vendor ID is provided (no product ID), all files for that vendor are deleted.
   *
   * @param options - Deletion criteria
   * @param options.filename - Specific filename to delete (e.g. `fff1.8000.prod.3`)
   * @param options.vendorId - Vendor ID to filter files for deletion
   * @param options.productId - Product ID to filter files for deletion (optional, requires vendorId)
   * @param options.isProduction - (deprecated, use `mode` instead) production (true) or test (false) mode
   * @param options.mode - Storage mode: "prod", "test", or "local"
   * @returns Number of files deleted
   */
  async delete(options) {
    if (this.#storage === void 0) {
      await this.construction;
    }
    const storage = this.#storage;
    const { vendorId, productId, isProduction } = options;
    let { filename, mode } = options;
    if (mode === void 0 && isProduction !== void 0) {
      mode = isProduction ? "prod" : "test";
    }
    if (filename !== void 0) {
      try {
        const fileDesignator = await this.fileDesignatorForUpdate(filename);
        await fileDesignator.delete();
      } catch (error) {
        OtaUpdateError.accept(error);
        return 0;
      }
      logger.info(`Deleted OTA file: ${filename}`);
      return 1;
    }
    const blobDriver = this.#blobDriver;
    const baseContexts = storage.baseContexts;
    if (vendorId !== void 0 && productId !== void 0 && mode !== void 0) {
      const vendorHex2 = vendorId.toString(16);
      const productHex = productId.toString(16);
      const modeContexts = [...baseContexts, vendorHex2, productHex, mode];
      const versionKeys = await blobDriver.keys(modeContexts);
      let deletedCount2 = 0;
      for (const versionKey of versionKeys) {
        const fd = new PersistedFileDesignator(versionKey, blobDriver, modeContexts);
        await fd.delete();
        deletedCount2++;
      }
      if (deletedCount2 > 0) {
        logger.info(`Deleted ${deletedCount2} OTA file(s) for ${vendorHex2}.${productHex}.${mode}`);
      }
      return deletedCount2;
    }
    if (vendorId === void 0) {
      throw new OtaUpdateError("Either filename or vendorId must be provided to delete files");
    }
    const vendorHex = vendorId.toString(16);
    const vendorContexts = [...baseContexts, vendorHex];
    let deletedCount = 0;
    const validModes = ["prod", "test", "local"];
    const modesToDelete = mode !== void 0 ? [mode] : validModes;
    for (const productKey of await blobDriver.contexts(vendorContexts)) {
      const productContexts = [...vendorContexts, productKey];
      const modeSubContexts = await blobDriver.contexts(productContexts);
      for (const modeStr of modesToDelete) {
        if (modeSubContexts.includes(modeStr)) {
          const modeContexts = [...productContexts, modeStr];
          for (const versionKey of await blobDriver.keys(modeContexts)) {
            const fd = new PersistedFileDesignator(versionKey, blobDriver, modeContexts);
            await fd.delete();
            deletedCount++;
          }
        }
      }
    }
    return deletedCount;
  }
}
export {
  DclOtaUpdateService,
  OtaUpdateError
};
//# sourceMappingURL=DclOtaUpdateService.js.map
