/*
 * Copyright (c) 2025, Salesforce, Inc.
 * SPDX-License-Identifier: Apache-2
 * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
 */
import { randomBytes } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { TelemetryReporter } from '@salesforce/telemetry';
import { getLogger } from '../logging/index.js';
const generateRandomId = () => randomBytes(20).toString('hex');
/**
 * Sanitize attributes to only include string, number, boolean (App Insights–safe).
 * Aligns with sf CLI telemetry record() validation.
 */
function sanitizeAttributes(attributes) {
    const out = {};
    for (const [key, value] of Object.entries(attributes)) {
        if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
            out[key] = value;
        }
    }
    return out;
}
/**
 * Get the path to the persistent CLI ID file.
 * @param dataDir - oclif dataDir for persistent storage
 */
const getCliIdPath = (dataDir) => {
    if (!dataDir)
        return null;
    return path.join(dataDir, 'cliid');
};
/**
 * Read or create a persistent CLI ID.
 * @param dataDir - oclif dataDir for persistent storage
 */
const readOrCreateCliId = (dataDir) => {
    const filePath = getCliIdPath(dataDir);
    if (!filePath)
        return generateRandomId();
    // Try to read existing ID
    try {
        if (fs.existsSync(filePath)) {
            const value = fs.readFileSync(filePath, 'utf8');
            const trimmed = value?.trim();
            if (trimmed)
                return trimmed;
        }
    }
    catch {
        // Fall through to create new
    }
    // Create new ID
    const newId = generateRandomId();
    try {
        const dir = path.dirname(filePath);
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
        }
        fs.writeFileSync(filePath, newId, { encoding: 'utf8', mode: 0o600 });
    }
    catch {
        // If we can't persist, still return the generated id
    }
    return newId;
};
/**
 * Custom TelemetryReporter that always enables telemetry.
 * Gating is handled at the instantiation site.
 */
class ConfigurableTelemetryReporter extends TelemetryReporter {
    isSfdxTelemetryEnabled() {
        return true;
    }
}
/**
 * Telemetry client for sending events to Application Insights.
 *
 * @example
 * ```typescript
 * const telemetry = new Telemetry({
 *   project: 'my-app',
 *   appInsightsKey: 'InstrumentationKey=...',
 *   version: '1.0.0',
 *   dataDir: '/path/to/data',
 * });
 *
 * await telemetry.start();
 * telemetry.sendEvent('USER_ACTION', { action: 'click' });
 * telemetry.stop();
 * ```
 */
export class Telemetry {
    attributes;
    cliId;
    project;
    reporter;
    sessionId;
    started;
    version;
    appInsightsKey;
    traceLog;
    /**
     * Check if telemetry is disabled via environment variables.
     * Supports both SF_DISABLE_TELEMETRY (sf CLI standard) and SFCC_DISABLE_TELEMETRY.
     */
    static isDisabled() {
        return process.env.SF_DISABLE_TELEMETRY === 'true' || process.env.SFCC_DISABLE_TELEMETRY === 'true';
    }
    /**
     * Get the connection string for telemetry, respecting disable flags and env overrides.
     * @param projectDefault - Default connection string from project config (e.g., package.json)
     * @returns Connection string to use, or undefined if telemetry should be disabled
     */
    static getConnectionString(projectDefault) {
        if (Telemetry.isDisabled())
            return undefined;
        return process.env.SFCC_APP_INSIGHTS_KEY ?? projectDefault;
    }
    constructor(options) {
        this.project = options.project;
        this.appInsightsKey = options.appInsightsKey;
        this.attributes = { ...(options.initialAttributes ?? {}) };
        this.cliId = readOrCreateCliId(options.dataDir);
        this.reporter = undefined;
        this.sessionId = generateRandomId();
        this.started = false;
        this.version = options.version ?? '0.0.0';
        if (process.env.SFCC_TELEMETRY_LOG === 'true') {
            this.traceLog = getLogger().child({ component: 'telemetry' });
        }
    }
    /**
     * Add additional attributes to include with all future events.
     */
    addAttributes(attributes) {
        this.traceLog?.debug({ attributes }, 'telemetry addAttributes');
        this.attributes = { ...this.attributes, ...attributes };
    }
    /**
     * Send a telemetry event. Events are buffered until you call {@link flush} or
     * {@link stop}. Use this for batching; use {@link sendEventAndFlush} when you
     * need one event sent before continuing.
     *
     * @param eventName - Name of the event (e.g., 'SERVER_STATUS', 'TOOL_CALLED')
     * @param attributes - Event-specific attributes (only string/number/boolean are sent)
     */
    sendEvent(eventName, attributes = {}) {
        try {
            const name = eventName?.trim() || 'UNKNOWN';
            this.traceLog?.debug({ event: name, attributes }, 'telemetry sendEvent');
            const eventProperties = this.buildEventProperties(attributes);
            this.reporter?.sendTelemetryEvent(name, eventProperties);
        }
        catch {
            // ignore send errors
        }
    }
    /**
     * Send a telemetry event and flush immediately. Use this when you need the event
     * delivered before continuing (e.g. after a tool call or server lifecycle event),
     * so you don't have to remember to call {@link flush}. For batching multiple
     * events and flushing once, use {@link sendEvent} and then {@link flush}.
     *
     * @param eventName - Name of the event (e.g., 'SERVER_STATUS', 'TOOL_CALLED')
     * @param attributes - Event-specific attributes (only string/number/boolean are sent)
     */
    async sendEventAndFlush(eventName, attributes = {}) {
        this.sendEvent(eventName, attributes);
        await this.flush();
    }
    /**
     * Send an exception to telemetry.
     *
     * @param error - The error to report
     * @param attributes - Additional attributes to include with the exception
     */
    sendException(error, attributes = {}) {
        try {
            this.traceLog?.debug({ error: error.name, message: error.message }, 'telemetry sendException');
            const properties = this.buildEventProperties(sanitizeAttributes(attributes));
            this.reporter?.sendTelemetryException(error, properties);
        }
        catch {
            // ignore send errors
        }
    }
    /**
     * Start the telemetry reporter.
     * Must be called before sending events.
     */
    async start() {
        if (this.started)
            return;
        this.started = true;
        // If no key provided, telemetry is disabled
        if (!this.appInsightsKey)
            return;
        this.traceLog?.debug({ project: this.project, sessionId: this.sessionId, cliId: this.cliId.slice(0, 8) + '...' }, 'telemetry start');
        try {
            await this.createReporter();
        }
        catch {
            // Best-effort retry after ~1s: first runs can hit transient failures
            // establishing the Application Insights connection (DNS/proxy/VPN warm-up,
            // brief network blips, or backend cold start). One short delay usually fixes it.
            // If the retry still fails, ignore it to avoid impacting the application.
            try {
                await this.createReporter();
            }
            catch {
                // ignore
            }
        }
    }
    /**
     * Flush pending telemetry events without stopping the reporter.
     * Use this for long-running processes that need to ensure events are sent periodically.
     * Uses the native reporter.flush() as documented in https://github.com/forcedotcom/telemetry,
     * and also flushes the App Insights client when present (SDK flush() only flushes O11y).
     */
    async flush() {
        if (!this.started || !this.reporter)
            return;
        await this.reporter.flush();
        // SDK flush() only flushes O11y; we use App Insights. Flush the underlying client.
        try {
            const client = this.reporter.getTelemetryClient();
            if (client?.flush) {
                await new Promise((resolve) => {
                    client.flush({ callback: () => resolve() });
                });
            }
        }
        catch {
            // getTelemetryClient() throws if App Insights not initialized
        }
    }
    /**
     * Stop the telemetry reporter and flush any pending events.
     * Includes a delay to allow async HTTP requests to complete.
     */
    async stop() {
        if (!this.started)
            return;
        this.traceLog?.debug('telemetry stop');
        this.started = false;
        this.reporter?.stop();
        // Allow pending HTTP requests to flush before process exits.
        // Application Insights SDK sends events asynchronously, so we need
        // a delay to ensure events reach the server on fast exits.
        // 300ms gives enough time for the HTTP request to complete even on
        // cold starts when establishing the initial connection.
        await new Promise((resolve) => setTimeout(resolve, 300));
    }
    buildEventProperties(attributes = {}) {
        const sanitized = sanitizeAttributes({ ...this.attributes, ...attributes });
        return {
            ...sanitized,
            sessionId: this.sessionId,
            cliId: this.cliId,
            version: this.version,
            platform: process.platform,
            arch: process.arch,
            nodeVersion: process.version,
            nodeEnv: process.env.NODE_ENV,
            origin: this.project,
            date: new Date().toUTCString(),
            timestamp: String(Date.now()),
            processUptime: process.uptime() * 1000,
        };
    }
    async createReporter() {
        this.reporter = await ConfigurableTelemetryReporter.create({
            project: this.project,
            key: this.appInsightsKey ?? '',
            userId: this.cliId,
        });
        this.reporter.start();
    }
}
//# sourceMappingURL=telemetry.js.map