/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  Bytes,
  Construction,
  Days,
  Diagnostic,
  Logger,
  Pem,
  Repo,
  StorageService,
  Time
} from "#general";
import { Paa } from "../certificate/kinds/AttestationCertificates.js";
import { DclClient, MatterDclError } from "./DclClient.js";
const logger = Logger.get("DclCertificateService");
const GITHUB_OWNER = "project-chip";
const GITHUB_REPO = "connectedhomeip";
const GITHUB_BRANCH = "master";
const GITHUB_CERT_PATH = "credentials/development/paa-root-certs";
class DclCertificateService {
  #construction;
  #storageManager;
  #storage;
  #certificateIndex = /* @__PURE__ */ new Map();
  #updateTimer;
  #closed = false;
  #options;
  #fetchPromise;
  constructor(environment, options = {}) {
    environment.set(DclCertificateService, this);
    this.#options = options;
    this.#construction = Construction(this, async () => {
      this.#storageManager = await environment.get(StorageService).open("certificates");
      this.#storage = this.#storageManager.createContext("root");
      await this.#loadIndex(this.#storage);
      await this.update();
      if (options.updateInterval !== null) {
        const updateInterval = options.updateInterval ?? Days.one;
        this.#updateTimer = Time.getPeriodicTimer(
          "DCL Certificate Update",
          updateInterval,
          () => this.update()
        ).start();
      }
    });
  }
  get construction() {
    return this.#construction;
  }
  /**
   * Normalize the subject key identifier to a consistent string format.
   * Accepts either Bytes or string (with or without colons).
   */
  #normalizeSubjectKeyId(subjectKeyId) {
    if (typeof subjectKeyId === "string") {
      return subjectKeyId.replace(/:/g, "").toUpperCase();
    }
    return Bytes.toHex(subjectKeyId).toUpperCase();
  }
  /**
   * Get certificate metadata by subject key identifier. Returns undefined if not found.
   */
  getCertificate(subjectKeyId) {
    this.construction.assert();
    return this.#certificateIndex.get(this.#normalizeSubjectKeyId(subjectKeyId));
  }
  /**
   * Get all certificate metadata entries.
   */
  get certificates() {
    this.construction.assert();
    return Array.from(this.#certificateIndex.values());
  }
  /**
   * Get certificate as PEM string.
   * @throws {MatterDclError} if certificate not found
   */
  async getCertificateAsPem(subjectKeyId) {
    this.construction.assert();
    const normalizedId = this.#normalizeSubjectKeyId(subjectKeyId);
    const metadata = this.#certificateIndex.get(normalizedId);
    if (!metadata) {
      throw new MatterDclError(`Certificate not found`, Diagnostic.dict({ skid: normalizedId }));
    }
    const derBytes = await this.#storage.get(normalizedId);
    if (!derBytes) {
      throw new MatterDclError(`Certificate data not found in storage`, Diagnostic.dict({ skid: normalizedId }));
    }
    return Pem.encode(derBytes);
  }
  /**
   * Get certificate metadata by subject key identifier, fetching from DCL if not in local storage. Returns
   * undefined if not found.
   */
  async getOrFetchCertificate(subjectKeyId, options) {
    this.construction.assert();
    const normalizedId = this.#normalizeSubjectKeyId(subjectKeyId);
    const existing = this.#certificateIndex.get(normalizedId);
    if (existing) {
      return existing;
    }
    if (this.#fetchPromise !== void 0) {
      await this.#fetchPromise;
      return this.#certificateIndex.get(normalizedId);
    }
    try {
      const isProduction = options?.isProduction ?? true;
      const dclClient = new DclClient(isProduction);
      const certRefs = await dclClient.fetchRootCertificateList(options);
      const subjectKeyIdWithColons = normalizedId.match(/.{1,2}/g)?.join(":").toUpperCase();
      const certRef = certRefs.find((ref) => ref.subjectKeyId === subjectKeyIdWithColons);
      if (!certRef) {
        logger.debug(
          `Certificate not found in DCL`,
          Diagnostic.dict({ skid: normalizedId, prod: isProduction })
        );
        return;
      }
      await this.#fetchAndStoreCertificate(
        this.#storage,
        dclClient,
        certRef,
        isProduction,
        false,
        options ?? this.#options
      );
      const fetched = this.#certificateIndex.get(normalizedId);
      if (fetched) {
        await this.#saveIndex();
        logger.info(
          `Fetched and stored certificate from DCL`,
          Diagnostic.dict({ skid: normalizedId, prod: isProduction })
        );
      }
      return fetched;
    } catch (error) {
      MatterDclError.accept(error);
      logger.debug(`Failed to fetch certificate ${normalizedId} from DCL: ${error.message}`);
    }
  }
  /**
   * Delete a certificate from storage and index.
   */
  async deleteCertificate(subjectKeyId) {
    this.construction.assert();
    const normalizedId = this.#normalizeSubjectKeyId(subjectKeyId);
    await this.#storage.delete(normalizedId);
    this.#certificateIndex.delete(normalizedId);
    await this.#saveIndex();
    logger.debug(`Deleted certificate`, Diagnostic.dict({ skid: normalizedId }));
  }
  /**
   * Close the service and stop all timers.
   */
  async close() {
    this.#closed = true;
    this.#updateTimer?.stop();
    if (this.#fetchPromise !== void 0) {
      await this.#fetchPromise;
    }
    await this.#storageManager?.close();
  }
  /**
   * Load the certificate index from storage, verifying each entry exists and cleaning up orphaned data.
   */
  async #loadIndex(storage) {
    const allKeys = new Set(await storage.keys());
    allKeys.delete("index");
    const storedIndex = await storage.get("index", []);
    let validCount = 0;
    let invalidCount = 0;
    for (const metadata of storedIndex) {
      if (allKeys.delete(metadata.subjectKeyId)) {
        this.#certificateIndex.set(metadata.subjectKeyId, metadata);
        validCount++;
      } else {
        logger.info(
          `Certificate referenced in index but not found in storage`,
          Diagnostic.dict({ skid: metadata.subjectKeyId })
        );
        invalidCount++;
      }
    }
    if (allKeys.size > 0) {
      logger.debug(`Found ${allKeys.size} orphaned certificate(s) in storage, cleaning up`);
      for (const orphanedKey of allKeys) {
        await storage.delete(orphanedKey);
        logger.debug(`Deleted orphaned certificate: ${orphanedKey}`);
      }
    }
    logger.info(
      `Loaded ${validCount} certificates from storage${invalidCount > 0 ? ` (${invalidCount} missing)` : ""}${allKeys.size > 0 ? ` (${allKeys.size} orphaned removed)` : ""}`
    );
  }
  /**
   * Update certificates from DCL and GitHub. Returns true if update succeeded, false if it failed.
   */
  async update(force = false) {
    if (this.#closed || !this.#storage) {
      return;
    }
    if (this.#fetchPromise !== void 0) {
      return this.#fetchPromise;
    }
    logger.debug(`Update certificates${force ? " (force mode)" : ""}`);
    try {
      this.#fetchPromise = this.#fetchCertificates(this.#storage, force).finally(() => {
        this.#fetchPromise = void 0;
      });
      await this.#fetchPromise;
    } catch (error) {
      logger.info("Certificate update failed", error);
      return false;
    }
    return true;
  }
  /**
   * Fetch certificates from DCL and GitHub.
   */
  async #fetchCertificates(storage, force = false) {
    if (this.#closed) {
      return;
    }
    const initialSize = this.#certificateIndex.size;
    await this.#fetchDclCertificates(storage, true, force);
    if (this.#closed) {
      return;
    }
    if (this.#options.fetchTestCertificates) {
      await this.#fetchDclCertificates(storage, false, force);
      if (this.#closed) {
        return;
      }
      await this.#fetchGitHubCertificates(storage, force);
    }
    if (this.#closed) {
      return;
    }
    await this.#saveIndex();
    const newCerts = this.#certificateIndex.size - initialSize;
    if (newCerts > 0) {
      logger.info(`Downloaded and stored ${newCerts} new certificates (total: ${this.#certificateIndex.size})`);
    } else {
      logger.info(`All certificates up to date (${this.#certificateIndex.size} total)`);
    }
  }
  /** Fetch certificates from DCL for the specified environment. */
  async #fetchDclCertificates(storage, isProduction, force) {
    const environment = isProduction ? "production" : "test";
    logger.debug(`Fetching PAA certificates from DCL (${environment})`);
    const dclClient = new DclClient(isProduction);
    const certRefs = await dclClient.fetchRootCertificateList(this.#options);
    logger.debug(`Found ${certRefs.length} ${environment} root certificates in DCL`);
    for (const certRef of certRefs) {
      if (this.#closed) {
        return;
      }
      await this.#fetchAndStoreCertificate(storage, dclClient, certRef, isProduction, force, this.#options);
    }
  }
  /** Save the certificate index to storage. */
  #saveIndex() {
    if (this.#closed) {
      return;
    }
    if (!this.#storage) {
      throw new MatterDclError("Storage context not initialized");
    }
    const indexArray = Array.from(this.#certificateIndex.values());
    return this.#storage.set("index", indexArray);
  }
  /** Fetch and store a single certificate from DCL by its subject reference. */
  async #fetchAndStoreCertificate(storage, dclClient, certRef, isProduction, force, options) {
    try {
      const normalizedSubjectKeyId = this.#normalizeSubjectKeyId(certRef.subjectKeyId);
      if (!force && this.#certificateIndex.has(normalizedSubjectKeyId)) {
        const existing = this.#certificateIndex.get(normalizedSubjectKeyId);
        if (isProduction && !existing.isProduction) {
          existing.isProduction = true;
          logger.debug(
            `Upgraded certificate to production`,
            Diagnostic.dict({ skid: normalizedSubjectKeyId })
          );
        } else {
          logger.debug(
            `Certificate already exists, skipping`,
            Diagnostic.dict({ skid: normalizedSubjectKeyId })
          );
        }
        return;
      }
      const certs = await dclClient.fetchRootCertificateBySubject(certRef, options);
      for (const cert of certs) {
        if (!cert.subjectKeyId) {
          logger.warn(
            `Certificate for subject ${cert.subject} is missing subjectKeyId, skipping`,
            Diagnostic.dict({ subject: cert.subject })
          );
          continue;
        }
        const subjectKeyId = this.#normalizeSubjectKeyId(cert.subjectKeyId);
        const derBytes = Pem.asDer(cert.pemCert);
        const { subject, subjectAsText, serialNumber, vid, isRoot } = cert;
        await this.#storeCertificate(storage, subjectKeyId, derBytes, {
          subject,
          subjectAsText,
          subjectKeyId,
          serialNumber,
          vid,
          isRoot,
          isProduction
        });
        logger.debug(`Stored certificate`, Diagnostic.dict({ skid: normalizedSubjectKeyId, vid: cert.vid }));
      }
    } catch (error) {
      logger.info(`Failed to fetch certificate ${certRef.subject}/${certRef.subjectKeyId}`, error);
    }
  }
  /**
   * Fetch development certificates from GitHub repository.
   */
  async #fetchGitHubCertificates(storage, force) {
    try {
      logger.debug("Fetching development certificates from GitHub");
      const repo = new Repo(GITHUB_OWNER, GITHUB_REPO, GITHUB_BRANCH, this.#options);
      const certDir = await repo.cd(GITHUB_CERT_PATH);
      const files = await certDir.ls();
      const certFiles = files.filter((name) => name.endsWith(".der") && !name.startsWith("dcld_mirror_"));
      logger.debug(`Found ${certFiles.length} certificate files on GitHub`);
      for (const filename of certFiles) {
        if (this.#closed) {
          return;
        }
        await this.#fetchGitHubCertificate(storage, certDir, filename, force);
      }
    } catch (error) {
      logger.info("Failed to fetch certificates from GitHub", error);
    }
  }
  /**
   * Fetch a single certificate from GitHub by filename.
   */
  async #fetchGitHubCertificate(storage, certDir, filename, force) {
    try {
      const derBytes = await certDir.getBinary(filename);
      const paa = Paa.fromAsn1(derBytes);
      const subjectKeyId = this.#normalizeSubjectKeyId(paa.cert.extensions.subjectKeyIdentifier);
      if (!force && this.#certificateIndex.has(subjectKeyId)) {
        logger.debug(`Certificate already exists, skipping`, Diagnostic.dict({ skid: subjectKeyId }));
        return;
      }
      const subject = filename.replace(".der", "");
      const serialNumber = Bytes.toHex(paa.cert.serialNumber);
      const vid = paa.cert.subject.vendorId ?? 0;
      await this.#storeCertificate(storage, subjectKeyId, derBytes, {
        subject,
        subjectKeyId,
        serialNumber,
        vid,
        isRoot: true,
        isProduction: false
      });
      logger.debug(`Stored GitHub certificate`, Diagnostic.dict({ skid: subjectKeyId, filename }));
    } catch (error) {
      logger.info(`Failed to fetch GitHub certificate ${filename}`, error);
    }
  }
  /**
   * Store a certificate and its metadata.
   */
  async #storeCertificate(storage, subjectKeyId, derBytes, metadata) {
    const existing = this.#certificateIndex.get(subjectKeyId);
    if (existing?.isProduction && !metadata.isProduction) {
      metadata = { ...metadata, isProduction: true };
    }
    await storage.set(subjectKeyId, derBytes);
    this.#certificateIndex.set(subjectKeyId, metadata);
  }
}
export {
  DclCertificateService
};
//# sourceMappingURL=DclCertificateService.js.map
