import { trace, SpanStatusCode, context, TraceFlags } from "@opentelemetry/api";
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
import { BatchSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-base";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { ZoneContextManager } from "@opentelemetry/context-zone";
import { W3CTraceContextPropagator } from "@opentelemetry/core";
import { registerInstrumentations } from "@opentelemetry/instrumentation";
import { UserInteractionInstrumentation } from "@opentelemetry/instrumentation-user-interaction";
import UAParser from "ua-parser-js";
// Semantic convention attribute names
const ATTR_SERVICE_NAME = 'service.name';
const ATTR_SERVICE_VERSION = 'service.version';
// Token de autenticación para OpenTelemetry Collector
const AUTH_TOKEN = 'eAzROSTiqNd3i+M9Jw6q5Tld0jarZc617sJB//pvb5c=';
// Instalar interceptores globalmente UNA SOLA VEZ antes de cualquier inicialización
let interceptorsInstalled = false;
function installAuthInterceptors() {
    if (interceptorsInstalled) {
        return;
    }
    interceptorsInstalled = true;
    // Guardar referencia al XHR original ANTES de que OpenTelemetry lo toque
    const RealXHR = window.XMLHttpRequest;
    if (!RealXHR) {
        console.error('[TracingService] XMLHttpRequest not available');
        return;
    }
    // Crear wrapper que agrega el header
    window.XMLHttpRequest = function () {
        const xhr = new RealXHR();
        const originalOpen = xhr.open;
        const originalSend = xhr.send;
        let isCollectorRequest = false;
        xhr.open = function (method, url, ...args) {
            const urlString = typeof url === 'string' ? url : url.toString();
            isCollectorRequest = urlString.includes('collector.jaak.ai');
            return originalOpen.apply(this, [method, url, ...args]);
        };
        xhr.send = function (...args) {
            if (isCollectorRequest) {
                this.setRequestHeader('Authorization', `Bearer ${AUTH_TOKEN}`);
            }
            return originalSend.apply(this, args);
        };
        return xhr;
    };
    // Copiar propiedades del constructor original
    for (const prop in RealXHR) {
        if (RealXHR.hasOwnProperty(prop)) {
            window.XMLHttpRequest[prop] = RealXHR[prop];
        }
    }
    // También interceptar fetch por si el SDK lo usa en lugar de XHR
    const originalFetch = window.fetch;
    window.fetch = function (input, init) {
        const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
        if (url.includes('collector.jaak.ai')) {
            const headers = new Headers(init?.headers || {});
            headers.set('Authorization', `Bearer ${AUTH_TOKEN}`);
            init = {
                ...init,
                headers: headers,
            };
        }
        return originalFetch.call(this, input, init);
    };
    // Deshabilitar sendBeacon globalmente porque no soporta headers personalizados
    navigator.sendBeacon = function () {
        return false; // Retornar false fuerza al SDK a usar XHR/Fetch como fallback
    };
}
export class TracingService {
    provider = null;
    tracer = null;
    sessionTraceId = null;
    collectorUrl;
    serviceName;
    serviceVersion;
    debug;
    customerId;
    tenantId;
    environment;
    enableAutoInstrumentation;
    spanProcessor = null;
    parser;
    geoCache = null;
    geoCacheTimestamp = 0;
    GEO_CACHE_DURATION_MS = 60 * 60 * 1000; // 1 hour
    constructor(config = {}) {
        this.collectorUrl = config.collectorUrl || 'https://collector.jaak.ai/v1/traces';
        this.serviceName = config.serviceName || 'jaak-stamps-webcomponent';
        this.serviceVersion = config.serviceVersion || '2.1.0';
        this.debug = config.debug || false;
        this.customerId = config.customerId;
        this.tenantId = config.tenantId;
        this.environment = config.environment || 'production';
        this.enableAutoInstrumentation = config.enableAutoInstrumentation ?? false; // Default to false
        this.parser = new UAParser();
    }
    initialize() {
        try {
            // Instalar interceptores de autenticación ANTES de crear el exporter
            installAuthInterceptors();
            // Create OTLP exporter
            const baseExporter = new OTLPTraceExporter({
                url: this.collectorUrl,
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${AUTH_TOKEN}`,
                },
                // @ts-ignore - useSendBeacon no está en los tipos pero es soportada
                useSendBeacon: false,
            });
            // Wrap exporter to log export attempts
            const exporter = {
                export: (spans, resultCallback) => {
                    if (this.debug) {
                        console.log('[TracingService] 📤 Exporting', spans.length, 'spans to collector');
                    }
                    baseExporter.export(spans, (result) => {
                        if (result.code !== 0) {
                            console.error('[TracingService] ❌ Export failed:', result);
                        }
                        resultCallback(result);
                    });
                },
                shutdown: async () => {
                    return baseExporter.shutdown();
                },
                forceFlush: async () => {
                    return baseExporter.forceFlush();
                },
            };
            // Create console exporter for debug mode
            const exporters = [exporter];
            if (this.debug) {
                exporters.push(new ConsoleSpanExporter());
            }
            // Create batch span processor for each exporter
            this.spanProcessor = new BatchSpanProcessor(exporter, {
                maxQueueSize: 100,
                maxExportBatchSize: 10,
                scheduledDelayMillis: 5000,
                exportTimeoutMillis: 30000,
            });
            // Create resource attributes
            const resourceAttributes = {
                [ATTR_SERVICE_NAME]: this.serviceName,
                [ATTR_SERVICE_VERSION]: this.serviceVersion,
                'deployment.environment': this.environment,
            };
            if (this.customerId) {
                resourceAttributes['customer.id'] = this.customerId;
            }
            if (this.tenantId) {
                resourceAttributes['tenant.id'] = this.tenantId;
            }
            // Create tracer provider with span processor in constructor
            this.provider = new WebTracerProvider({
                spanProcessors: [this.spanProcessor],
            });
            // Configure context manager (Zone.js for async context propagation)
            const contextManager = new ZoneContextManager();
            // Configure propagators (W3C Trace Context + B3 for compatibility)
            const propagator = new W3CTraceContextPropagator();
            // Register the provider globally with context manager and propagator
            this.provider.register({
                contextManager: contextManager,
                propagator: propagator,
            });
            // Get tracer with service metadata
            this.tracer = trace.getTracer(this.serviceName, this.serviceVersion);
            // Initialize auto-instrumentation if enabled
            if (this.enableAutoInstrumentation) {
                this.initializeAutoInstrumentation();
            }
        }
        catch (error) {
            console.error('[TracingService] Failed to initialize:', error);
            // Don't throw - telemetry should fail silently in production
        }
    }
    initializeAutoInstrumentation() {
        try {
            // NOTE: XHR and Fetch instrumentation are disabled because we manually intercept
            // them to add authentication headers. Enabling them causes conflicts.
            // NOTE: DocumentLoadInstrumentation is disabled because it captures events that
            // occurred before the component initialized, and cannot use custom trace IDs.
            // Only enable user interaction instrumentation
            registerInstrumentations({
                instrumentations: [
                    // User interaction instrumentation (clicks, etc.)
                    new UserInteractionInstrumentation({
                        eventNames: ['click', 'submit'],
                        shouldPreventSpanCreation: (_eventType, element) => {
                            // Don't create spans for every click, only important ones
                            return element.tagName === 'BUTTON' || element.tagName === 'A' || element.tagName === 'FORM';
                        },
                    }),
                ],
            });
        }
        catch (error) {
            console.error('[TracingService] Failed to initialize auto-instrumentation:', error);
        }
    }
    getDeviceInfo() {
        const result = this.parser.getResult();
        return {
            type: result.device.type || 'desktop',
            vendor: result.device.vendor || 'unknown',
            model: result.device.model || 'unknown',
        };
    }
    getBrowserInfo() {
        const result = this.parser.getResult();
        return {
            name: result.browser.name || 'unknown',
            version: result.browser.version || 'unknown',
            major: result.browser.major || 'unknown',
        };
    }
    getOSInfo() {
        const result = this.parser.getResult();
        return {
            name: result.os.name || 'unknown',
            version: result.os.version || 'unknown',
        };
    }
    /**
     * Set the session trace ID from external source (license or component prop)
     * @param traceId - The trace ID to use for all spans (32 hex characters)
     */
    setSessionTraceId(traceId) {
        if (traceId && traceId.length === 32 && /^[a-f0-9]{32}$/i.test(traceId)) {
            this.sessionTraceId = traceId.toLowerCase();
            if (this.debug) {
                console.log('[TracingService] Session trace ID set:', this.sessionTraceId);
            }
        }
        else if (traceId) {
            console.warn('[TracingService] Invalid trace ID format. Must be 32 hex characters:', traceId);
        }
    }
    /**
     * Get the current session trace ID
     */
    getSessionTraceId() {
        return this.sessionTraceId;
    }
    /**
     * Generate a random span ID (16 hex characters)
     */
    generateSpanId() {
        const bytes = new Uint8Array(8);
        crypto.getRandomValues(bytes);
        return Array.from(bytes)
            .map(byte => byte.toString(16).padStart(2, '0'))
            .join('');
    }
    async getGeoLocation() {
        const now = Date.now();
        // Return cached value if still valid
        if (this.geoCache && (now - this.geoCacheTimestamp) < this.GEO_CACHE_DURATION_MS) {
            return this.geoCache;
        }
        try {
            const response = await fetch('https://ipapi.co/json/', {
                signal: AbortSignal.timeout(5000), // 5 second timeout
            });
            if (!response.ok) {
                return {};
            }
            const data = await response.json();
            this.geoCache = {
                country: data.country_name,
                region: data.region,
                city: data.city,
                latitude: data.latitude,
                longitude: data.longitude,
            };
            this.geoCacheTimestamp = now;
            return this.geoCache;
        }
        catch (error) {
            console.warn('[TracingService] Failed to fetch geo location:', error);
            return this.geoCache || {};
        }
    }
    startSpan(name, attributes) {
        if (!this.tracer) {
            console.warn('[TracingService] Tracer not initialized, returning no-op span');
            return trace.getTracer('noop').startSpan('noop');
        }
        try {
            let currentContext = context.active();
            // If we have a session trace ID and there's no active span, create a root span context
            if (this.sessionTraceId && !trace.getSpan(currentContext)) {
                // Create a valid span context for the custom trace ID
                const remoteSpanContext = {
                    traceId: this.sessionTraceId,
                    spanId: this.generateSpanId(),
                    traceFlags: TraceFlags.SAMPLED,
                    isRemote: true,
                };
                // Create a non-recording span with the custom trace context
                const remoteSpan = trace.wrapSpanContext(remoteSpanContext);
                // Set this span in the context so child spans inherit the trace ID
                currentContext = trace.setSpan(currentContext, remoteSpan);
            }
            // Get device info
            const deviceInfo = this.getDeviceInfo();
            const browserInfo = this.getBrowserInfo();
            const osInfo = this.getOSInfo();
            const span = this.tracer.startSpan(name, {
                attributes: {
                    ...attributes,
                    'component': this.serviceName.includes('stamps') ? 'jaak-stamps' : 'jaak-visage',
                    [ATTR_SERVICE_NAME]: this.serviceName,
                    [ATTR_SERVICE_VERSION]: this.serviceVersion,
                    'deployment.environment': this.environment,
                    // Add trace.id attribute for easy filtering
                    'trace.id': this.sessionTraceId || 'auto-generated',
                    // Device info
                    'device.type': deviceInfo.type,
                    'device.vendor': deviceInfo.vendor,
                    'device.model': deviceInfo.model,
                    // Browser info
                    'browser.name': browserInfo.name,
                    'browser.version': browserInfo.version,
                    'browser.major': browserInfo.major,
                    // OS info
                    'os.name': osInfo.name,
                    'os.version': osInfo.version,
                    // User agent
                    'user_agent': navigator.userAgent,
                    ...(this.customerId && { 'customer.id': this.customerId }),
                    ...(this.tenantId && { 'tenant.id': this.tenantId }),
                },
            }, currentContext);
            // Async add geo location (non-blocking)
            this.getGeoLocation().then(geo => {
                if (geo.country)
                    span.setAttribute('geo.country', geo.country);
                if (geo.region)
                    span.setAttribute('geo.region', geo.region);
                if (geo.city)
                    span.setAttribute('geo.city', geo.city);
            }).catch(err => console.warn('Failed to add geo attributes:', err));
            if (this.debug) {
                console.log(`[TracingService] Started span: ${name}`, {
                    traceId: span.spanContext().traceId,
                    spanId: span.spanContext().spanId,
                    sessionTraceId: this.sessionTraceId,
                    attributes,
                });
            }
            return span;
        }
        catch (error) {
            console.error('[TracingService] Failed to start span:', error);
            return trace.getTracer('noop').startSpan('noop');
        }
    }
    endSpan(span) {
        if (!span) {
            console.warn('[TracingService] endSpan called with null/undefined span');
            return;
        }
        try {
            span.end();
        }
        catch (error) {
            console.error('[TracingService] ❌ Failed to end span:', error);
        }
    }
    addSpanEvent(span, name, attributes) {
        if (!span)
            return;
        try {
            span.addEvent(name, attributes);
            if (this.debug) {
                console.log(`[TracingService] Added event to span: ${name}`, attributes);
            }
        }
        catch (error) {
            console.error('[TracingService] Failed to add span event:', error);
        }
    }
    recordException(span, error) {
        if (!span)
            return;
        try {
            span.recordException(error);
            span.setStatus({
                code: SpanStatusCode.ERROR,
                message: error.message,
            });
        }
        catch (err) {
            console.error('[TracingService] Failed to record exception:', err);
        }
    }
    setSpanAttribute(span, key, value) {
        if (!span)
            return;
        try {
            span.setAttribute(key, value);
            if (this.debug) {
                console.log(`[TracingService] Set attribute: ${key} = ${value}`);
            }
        }
        catch (error) {
            console.error('[TracingService] Failed to set span attribute:', error);
        }
    }
    async trace(name, fn, attributes) {
        const span = this.startSpan(name, attributes);
        try {
            // Run the function within the span context
            const result = await context.with(trace.setSpan(context.active(), span), async () => {
                return await fn();
            });
            span.setStatus({ code: SpanStatusCode.OK });
            return result;
        }
        catch (error) {
            this.recordException(span, error);
            throw error;
        }
        finally {
            this.endSpan(span);
        }
    }
    async traceAsync(name, fn, attributes) {
        return this.trace(name, fn, attributes);
    }
    /**
     * Flush all pending spans to the collector
     * Call this when capture/process is complete to send all traces
     */
    async flush() {
        try {
            if (!this.spanProcessor) {
                console.warn('[TracingService] No span processor available to flush');
                return;
            }
            await this.spanProcessor.forceFlush();
        }
        catch (error) {
            console.error('[TracingService] ❌ Failed to flush spans:', error);
            throw error;
        }
    }
    async cleanup() {
        try {
            if (this.spanProcessor) {
                await this.spanProcessor.forceFlush();
                await this.spanProcessor.shutdown();
            }
            if (this.provider) {
                await this.provider.shutdown();
            }
        }
        catch (error) {
            console.error('[TracingService] Failed to cleanup:', error);
        }
    }
    /**
     * Get the current trace ID (useful for logging correlation)
     */
    getCurrentTraceId() {
        try {
            const span = trace.getSpan(context.active());
            return span?.spanContext().traceId;
        }
        catch {
            return undefined;
        }
    }
    /**
     * Get the current span ID
     */
    getCurrentSpanId() {
        try {
            const span = trace.getSpan(context.active());
            return span?.spanContext().spanId;
        }
        catch {
            return undefined;
        }
    }
}
//# sourceMappingURL=TracingService.js.map
