/**
 * Singleton secret scanner service with caching, concurrency control,
 * and availability gating.
 *
 * Wraps the low-level TruffleHog process-spawning logic from
 * `secret-scanner.ts` to prevent resource exhaustion in containers:
 *
 * - **Caching**: Content-hash keyed results avoid re-scanning identical
 *   content within a TTL window.
 * - **In-flight deduplication**: Concurrent scans of identical content
 *   coalesce into a single TruffleHog invocation.
 * - **Concurrency control**: A promise-based semaphore limits the number
 *   of concurrent TruffleHog child processes.
 * - **Availability gating**: When TruffleHog is not installed, all scans
 *   immediately return empty findings without spawning processes or
 *   logging errors.
 */
import { createHash } from "node:crypto";
import { getErrorMeta, getPrefixedLogger } from "../../util/logger.js";
import { isTrufflehogAvailable as checkTrufflehogAvailable, runTrufflehog, redactSecrets, } from "./secret-scanner.js";
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** Default timeout for a single TruffleHog scan (ms). */
export const DEFAULT_SCAN_TIMEOUT_MS = 10_000;
/** Maximum number of cached scan results. */
export const MAX_CACHE_SIZE = 200;
/** Time-to-live for cached scan results (ms). */
export const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
/** Maximum concurrent TruffleHog child processes. */
export const MAX_CONCURRENCY = 2;
// ---------------------------------------------------------------------------
// SecretScannerService
// ---------------------------------------------------------------------------
/**
 * Singleton service that wraps TruffleHog process spawning with caching,
 * in-flight deduplication, concurrency control, and availability gating.
 *
 * Not intended to be instantiated directly — use the module-level
 * {@link scanContentForSecrets} and {@link isTrufflehogAvailable} functions.
 */
class SecretScannerService {
    /** Content hash -> cached findings. */
    cache = new Map();
    /** Content hash -> in-flight scan promise (coalesces duplicate requests). */
    inflight = new Map();
    /** Number of TruffleHog processes currently running. */
    activeProcesses = 0;
    /** Waiters blocked on a concurrency slot. */
    waitQueue = [];
    /**
     * Tri-state availability flag:
     * - `undefined` = not yet checked
     * - `true`      = TruffleHog is installed
     * - `false`     = TruffleHog is not installed
     */
    available;
    /**
     * Scan content for secrets, applying caching and concurrency control.
     *
     * @param content - Text to scan
     * @param options - Scan configuration
     * @returns Scan result with findings and redacted content
     */
    async scan(content, options = {}) {
        const logger = getPrefixedLogger("[secret-scan]");
        const { timeout = DEFAULT_SCAN_TIMEOUT_MS, verifiedOnly = false } = options;
        // 1. Availability gate — only cache positive availability so transient
        //    failures (timeouts, SIGTERM) don't permanently disable scanning.
        if (this.available !== true) {
            const isAvailable = await checkTrufflehogAvailable();
            if (isAvailable) {
                this.available = true;
            }
            else {
                logger.debug("TruffleHog not available; secret scanning skipped");
                return {
                    success: false,
                    findings: [],
                    error: "Secret scanning unavailable: TruffleHog is not available",
                    hasVerifiedSecrets: false,
                    hasSecrets: false,
                    redactedContent: content,
                };
            }
        }
        // 2. Cache lookup
        const cacheKey = this.hashContent(content, verifiedOnly);
        const cached = this.cache.get(cacheKey);
        if (cached && cached.expiresAt > Date.now()) {
            const findings = cached.findings;
            return {
                success: true,
                findings,
                hasVerifiedSecrets: findings.some((f) => f.verified),
                hasSecrets: findings.length > 0,
                redactedContent: redactSecrets(content, findings),
            };
        }
        // 3. In-flight deduplication — piggyback callers must also handle
        //    rejection so they return { success: false } instead of throwing.
        const existing = this.inflight.get(cacheKey);
        if (existing) {
            try {
                const findings = await existing;
                return {
                    success: true,
                    findings,
                    hasVerifiedSecrets: findings.some((f) => f.verified),
                    hasSecrets: findings.length > 0,
                    redactedContent: redactSecrets(content, findings),
                };
            }
            catch (error) {
                logger.error("Secret scan failed (deduped)", getErrorMeta(error));
                return {
                    success: false,
                    findings: [],
                    error: error instanceof Error ? error.message : String(error),
                    hasVerifiedSecrets: false,
                    hasSecrets: false,
                    redactedContent: content,
                };
            }
        }
        // 4. Spawn TruffleHog with concurrency control
        const scanPromise = this.runWithConcurrencyControl(content, timeout, verifiedOnly, cacheKey);
        this.inflight.set(cacheKey, scanPromise);
        try {
            const findings = await scanPromise;
            return {
                success: true,
                findings,
                hasVerifiedSecrets: findings.some((f) => f.verified),
                hasSecrets: findings.length > 0,
                redactedContent: redactSecrets(content, findings),
            };
        }
        catch (error) {
            logger.error("Secret scan failed", getErrorMeta(error));
            return {
                success: false,
                findings: [],
                error: error instanceof Error ? error.message : String(error),
                hasVerifiedSecrets: false,
                hasSecrets: false,
                redactedContent: content,
            };
        }
        finally {
            this.inflight.delete(cacheKey);
        }
    }
    /**
     * Check if TruffleHog is available. Only caches positive results so
     * transient failures don't permanently disable scanning.
     */
    async isAvailable() {
        if (this.available !== true) {
            const isAvailable = await checkTrufflehogAvailable();
            if (isAvailable) {
                this.available = true;
            }
            return isAvailable;
        }
        return true;
    }
    /**
     * Dispose of the singleton — clears the cache and resets availability.
     */
    dispose() {
        this.cache.clear();
        this.inflight.clear();
        this.available = undefined;
    }
    // -----------------------------------------------------------------------
    // Private helpers
    // -----------------------------------------------------------------------
    /**
     * Compute a cache key from content and scan flags.
     */
    hashContent(content, verifiedOnly) {
        const hash = createHash("sha256");
        hash.update(content);
        hash.update(verifiedOnly ? ":v" : ":a");
        return hash.digest("hex");
    }
    /**
     * Acquire a concurrency slot, run TruffleHog, cache the result, and
     * release the slot.
     */
    async runWithConcurrencyControl(content, timeout, verifiedOnly, cacheKey) {
        await this.acquireSlot();
        try {
            const findings = await runTrufflehog(content, timeout, verifiedOnly);
            // Cache successful results
            this.cacheResult(cacheKey, findings);
            return findings;
        }
        finally {
            this.releaseSlot();
        }
    }
    /**
     * Wait until a concurrency slot is available.
     */
    async acquireSlot() {
        if (this.activeProcesses < MAX_CONCURRENCY) {
            this.activeProcesses++;
            return;
        }
        return new Promise((resolve) => {
            this.waitQueue.push(resolve);
        });
    }
    /**
     * Release a concurrency slot and wake the next waiter.
     */
    releaseSlot() {
        const next = this.waitQueue.shift();
        if (next) {
            // Hand the slot directly to the next waiter
            next();
        }
        else {
            this.activeProcesses--;
        }
    }
    /**
     * Store findings in the cache, evicting the oldest entry if at capacity.
     */
    cacheResult(key, findings) {
        if (this.cache.size >= MAX_CACHE_SIZE) {
            // Evict the oldest entry (first key in insertion order)
            const oldest = this.cache.keys().next().value;
            if (oldest !== undefined) {
                this.cache.delete(oldest);
            }
        }
        this.cache.set(key, {
            findings,
            expiresAt: Date.now() + CACHE_TTL_MS,
        });
    }
}
// ---------------------------------------------------------------------------
// Module-level singleton
// ---------------------------------------------------------------------------
let instance;
function getInstance() {
    if (!instance) {
        instance = new SecretScannerService();
    }
    return instance;
}
/**
 * Scans text content for secrets using TruffleHog, with caching,
 * concurrency control, and availability gating.
 *
 * Drop-in replacement for the original `scanContentForSecrets` from
 * `secret-scanner.ts` — same signature, same return type.
 *
 * @param content - The text content to scan for secrets
 * @param options - Scan configuration options
 * @returns Promise resolving to scan results with any findings
 */
export async function scanContentForSecrets(content, options = {}) {
    return getInstance().scan(content, options);
}
/**
 * Checks if TruffleHog is available on the system (cached after first call).
 *
 * @returns Promise resolving to true if TruffleHog is installed
 */
export async function isTrufflehogAvailable() {
    return getInstance().isAvailable();
}
/**
 * Disposes the singleton scanner service, clearing all caches and
 * resetting the availability flag.
 *
 * Primarily useful in tests or graceful shutdown.
 */
export function disposeSecretScanner() {
    if (instance) {
        instance.dispose();
        instance = undefined;
    }
}
//# sourceMappingURL=secret-scanner-service.js.map