/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { DiagLogLevel, diag, trace, context, metrics, propagation, } from '@opentelemetry/api';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
import { OTLPTraceExporter as OTLPTraceExporterHttp } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPLogExporter as OTLPLogExporterHttp } from '@opentelemetry/exporter-logs-otlp-http';
import { OTLPMetricExporter as OTLPMetricExporterHttp } from '@opentelemetry/exporter-metrics-otlp-http';
import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { BatchSpanProcessor, ConsoleSpanExporter, } from '@opentelemetry/sdk-trace-node';
import { BatchLogRecordProcessor, ConsoleLogRecordExporter, } from '@opentelemetry/sdk-logs';
import { ConsoleMetricExporter, PeriodicExportingMetricReader, } from '@opentelemetry/sdk-metrics';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { SERVICE_NAME } from './constants.js';
import { initializeMetrics } from './metrics.js';
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js';
import { FileLogExporter, FileMetricExporter, FileSpanExporter, } from './file-exporters.js';
import { GcpTraceExporter, GcpMetricExporter, GcpLogExporter, } from './gcp-exporters.js';
import { TelemetryTarget } from './index.js';
import { debugLogger } from '../utils/debugLogger.js';
import { authEvents } from '../code_assist/oauth2.js';
import { coreEvents, CoreEvent } from '../utils/events.js';
import { logKeychainAvailability, logTokenStorageInitialization, } from './loggers.js';
// For troubleshooting, set the log level to DiagLogLevel.DEBUG
class DiagLoggerAdapter {
    error(message, ...args) {
        debugLogger.error(message, ...args);
    }
    warn(message, ...args) {
        debugLogger.warn(message, ...args);
    }
    info(message, ...args) {
        debugLogger.log(message, ...args);
    }
    debug(message, ...args) {
        debugLogger.debug(message, ...args);
    }
    verbose(message, ...args) {
        debugLogger.debug(message, ...args);
    }
}
diag.setLogger(new DiagLoggerAdapter(), DiagLogLevel.INFO);
let sdk;
let spanProcessor;
let logRecordProcessor;
let telemetryInitialized = false;
let callbackRegistered = false;
let authListener = undefined;
let keychainAvailabilityListener = undefined;
let tokenStorageTypeListener = undefined;
const telemetryBuffer = [];
let activeTelemetryEmail;
export function isTelemetrySdkInitialized() {
    return telemetryInitialized;
}
export function bufferTelemetryEvent(fn) {
    if (telemetryInitialized) {
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
        fn();
    }
    else {
        telemetryBuffer.push(fn);
    }
}
async function flushTelemetryBuffer() {
    if (!telemetryInitialized)
        return;
    while (telemetryBuffer.length > 0) {
        const fn = telemetryBuffer.shift();
        if (fn) {
            try {
                await fn();
            }
            catch (e) {
                debugLogger.error('Error executing buffered telemetry event', e);
            }
        }
    }
}
function parseOtlpEndpoint(otlpEndpointSetting, protocol) {
    if (!otlpEndpointSetting) {
        return undefined;
    }
    // Trim leading/trailing quotes that might come from env variables
    const trimmedEndpoint = otlpEndpointSetting.replace(/^["']|["']$/g, '');
    try {
        const url = new URL(trimmedEndpoint);
        if (protocol === 'grpc') {
            // OTLP gRPC exporters expect an endpoint in the format scheme://host:port
            // The `origin` property provides this, stripping any path, query, or hash.
            return url.origin;
        }
        // For http, use the full href.
        return url.href;
    }
    catch (error) {
        diag.error('Invalid OTLP endpoint URL provided:', trimmedEndpoint, error);
        return undefined;
    }
}
export async function initializeTelemetry(config, credentials) {
    if (!config.getTelemetryEnabled()) {
        return;
    }
    if (telemetryInitialized) {
        if (credentials?.client_email &&
            activeTelemetryEmail &&
            credentials.client_email !== activeTelemetryEmail) {
            const message = `Telemetry credentials have changed (from ${activeTelemetryEmail} to ${credentials.client_email}), but telemetry cannot be re-initialized in this process. Please restart the CLI to use the new account for telemetry.`;
            debugLogger.error(message);
        }
        return;
    }
    if (config.getTelemetryUseCollector() && config.getTelemetryUseCliAuth()) {
        debugLogger.error('Telemetry configuration error: "useCollector" and "useCliAuth" cannot both be true. ' +
            'CLI authentication is only supported with in-process exporters. ' +
            'Disabling telemetry.');
        return;
    }
    // If using CLI auth and no credentials provided, defer initialization
    if (config.getTelemetryUseCliAuth() && !credentials) {
        // Register a callback to initialize telemetry when the user logs in.
        // This is done only once.
        if (!callbackRegistered) {
            callbackRegistered = true;
            authListener = async (newCredentials) => {
                if (config.getTelemetryEnabled() && config.getTelemetryUseCliAuth()) {
                    debugLogger.log('Telemetry reinit with credentials.');
                    await initializeTelemetry(config, newCredentials);
                }
            };
            authEvents.on('post_auth', authListener);
        }
        debugLogger.log('CLI auth is requested but no credentials, deferring telemetry initialization.');
        return;
    }
    const resource = resourceFromAttributes({
        [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME,
        [SemanticResourceAttributes.SERVICE_VERSION]: process.version,
        'session.id': config.getSessionId(),
    });
    if (!keychainAvailabilityListener) {
        keychainAvailabilityListener = (event) => {
            logKeychainAvailability(config, event);
        };
        coreEvents.on(CoreEvent.TelemetryKeychainAvailability, keychainAvailabilityListener);
    }
    if (!tokenStorageTypeListener) {
        tokenStorageTypeListener = (event) => {
            logTokenStorageInitialization(config, event);
        };
        coreEvents.on(CoreEvent.TelemetryTokenStorageType, tokenStorageTypeListener);
    }
    const otlpEndpoint = config.getTelemetryOtlpEndpoint();
    const otlpProtocol = config.getTelemetryOtlpProtocol();
    const telemetryTarget = config.getTelemetryTarget();
    const useCollector = config.getTelemetryUseCollector();
    const parsedEndpoint = parseOtlpEndpoint(otlpEndpoint, otlpProtocol);
    const telemetryOutfile = config.getTelemetryOutfile();
    const useOtlp = !!parsedEndpoint && !telemetryOutfile;
    const gcpProjectId = process.env['OTLP_GOOGLE_CLOUD_PROJECT'] ||
        process.env['GOOGLE_CLOUD_PROJECT'];
    const useDirectGcpExport = telemetryTarget === TelemetryTarget.GCP && !useCollector;
    let spanExporter;
    let logExporter;
    let metricReader;
    if (useDirectGcpExport) {
        debugLogger.log('Creating GCP exporters with projectId:', gcpProjectId, 'using', credentials ? 'provided credentials' : 'ADC');
        spanExporter = new GcpTraceExporter(gcpProjectId, credentials);
        logExporter = new GcpLogExporter(gcpProjectId, credentials);
        metricReader = new PeriodicExportingMetricReader({
            exporter: new GcpMetricExporter(gcpProjectId, credentials),
            exportIntervalMillis: 30000,
        });
    }
    else if (useOtlp) {
        if (otlpProtocol === 'http') {
            const buildUrl = (path) => {
                const url = new URL(parsedEndpoint);
                // Join the existing pathname with the new path, handling trailing slashes.
                url.pathname = [url.pathname.replace(/\/$/, ''), path].join('/');
                return url.href;
            };
            spanExporter = new OTLPTraceExporterHttp({
                url: buildUrl('v1/traces'),
            });
            logExporter = new OTLPLogExporterHttp({
                url: buildUrl('v1/logs'),
            });
            metricReader = new PeriodicExportingMetricReader({
                exporter: new OTLPMetricExporterHttp({
                    url: buildUrl('v1/metrics'),
                }),
                exportIntervalMillis: 10000,
            });
        }
        else {
            // grpc
            spanExporter = new OTLPTraceExporter({
                url: parsedEndpoint,
                compression: CompressionAlgorithm.GZIP,
            });
            logExporter = new OTLPLogExporter({
                url: parsedEndpoint,
                compression: CompressionAlgorithm.GZIP,
            });
            metricReader = new PeriodicExportingMetricReader({
                exporter: new OTLPMetricExporter({
                    url: parsedEndpoint,
                    compression: CompressionAlgorithm.GZIP,
                }),
                exportIntervalMillis: 10000,
            });
        }
    }
    else if (telemetryOutfile) {
        spanExporter = new FileSpanExporter(telemetryOutfile);
        logExporter = new FileLogExporter(telemetryOutfile);
        metricReader = new PeriodicExportingMetricReader({
            exporter: new FileMetricExporter(telemetryOutfile),
            exportIntervalMillis: 10000,
        });
    }
    else {
        spanExporter = new ConsoleSpanExporter();
        logExporter = new ConsoleLogRecordExporter();
        metricReader = new PeriodicExportingMetricReader({
            exporter: new ConsoleMetricExporter(),
            exportIntervalMillis: 10000,
        });
    }
    // Store processor references for manual flushing
    spanProcessor = new BatchSpanProcessor(spanExporter);
    logRecordProcessor = new BatchLogRecordProcessor(logExporter);
    sdk = new NodeSDK({
        resource,
        spanProcessors: [spanProcessor],
        logRecordProcessors: [logRecordProcessor],
        metricReader,
        instrumentations: [new HttpInstrumentation()],
    });
    try {
        sdk.start();
        if (config.getDebugMode()) {
            debugLogger.log('OpenTelemetry SDK started successfully.');
        }
        activeTelemetryEmail = credentials?.client_email;
        initializeMetrics(config);
        telemetryInitialized = true;
        void flushTelemetryBuffer();
    }
    catch (error) {
        debugLogger.error('Error starting OpenTelemetry SDK:', error);
    }
    // Note: We don't use process.on('exit') here because that callback is synchronous
    // and won't wait for the async shutdownTelemetry() to complete.
    // Instead, telemetry shutdown is handled in runExitCleanup() in cleanup.ts
    process.on('SIGTERM', () => {
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
        shutdownTelemetry(config);
    });
    process.on('SIGINT', () => {
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
        shutdownTelemetry(config);
    });
}
/**
 * Force flush all pending telemetry data to disk.
 * This is useful for ensuring telemetry is written before critical operations like /clear.
 */
export async function flushTelemetry(config) {
    if (!telemetryInitialized || !spanProcessor || !logRecordProcessor) {
        return;
    }
    try {
        // Force flush all pending telemetry to disk
        await Promise.all([
            spanProcessor.forceFlush(),
            logRecordProcessor.forceFlush(),
        ]);
        if (config.getDebugMode()) {
            debugLogger.log('OpenTelemetry SDK flushed successfully.');
        }
    }
    catch (error) {
        debugLogger.error('Error flushing SDK:', error);
    }
}
export async function shutdownTelemetry(config, fromProcessExit = true) {
    if (!telemetryInitialized || !sdk) {
        return;
    }
    try {
        ClearcutLogger.getInstance()?.shutdown();
        await sdk.shutdown();
        if (config.getDebugMode() && fromProcessExit) {
            debugLogger.log('OpenTelemetry SDK shut down successfully.');
        }
    }
    catch (error) {
        debugLogger.error('Error shutting down SDK:', error);
    }
    finally {
        telemetryInitialized = false;
        sdk = undefined;
        // Fully reset the global APIs to allow for re-initialization.
        // This is primarily for testing environments where the SDK is started
        // and stopped multiple times in the same process.
        trace.disable();
        context.disable();
        metrics.disable();
        propagation.disable();
        diag.disable();
        if (authListener) {
            authEvents.off('post_auth', authListener);
            authListener = undefined;
        }
        if (keychainAvailabilityListener) {
            coreEvents.off(CoreEvent.TelemetryKeychainAvailability, keychainAvailabilityListener);
            keychainAvailabilityListener = undefined;
        }
        if (tokenStorageTypeListener) {
            coreEvents.off(CoreEvent.TelemetryTokenStorageType, tokenStorageTypeListener);
            tokenStorageTypeListener = undefined;
        }
        callbackRegistered = false;
        activeTelemetryEmail = undefined;
    }
}
//# sourceMappingURL=sdk.js.map