import { resourceFromAttributes } from '@opentelemetry/resources';
import { W3CTraceContextPropagator } from '@opentelemetry/core';
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import * as SemanticAttributes from '@opentelemetry/semantic-conventions';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
import { SessionType, ATTR_MULTIPLAYER_SESSION_ID, SessionRecorderIdGenerator, SessionRecorderBrowserTraceExporter, SessionRecorderTraceIdRatioBasedSampler, } from '@multiplayer-app/session-recorder-common';
import { SessionRecorderSdk } from '@multiplayer-app/session-recorder-common';
import { trace, SpanStatusCode, context } from '@opentelemetry/api';
import { OTEL_IGNORE_URLS } from '../config';
import { processHttpPayload, headersToObject, extractResponseBody, getExporterEndpoint, getElementTextContent, } from './helpers';
export class TracerBrowserSDK {
    constructor() {
        this.sessionId = '';
        this.globalErrorListenersRegistered = false;
    }
    setSessionId(sessionId, sessionType = SessionType.PLAIN) {
        this.sessionId = sessionId;
        this.idGenerator.setSessionId(sessionId, sessionType);
    }
    init(options) {
        this.config = options;
        const { application, version, environment } = this.config;
        this.idGenerator = new SessionRecorderIdGenerator();
        this.exporter = new SessionRecorderBrowserTraceExporter({
            apiKey: options.apiKey,
            url: getExporterEndpoint(options.exporterEndpoint),
            usePostMessageFallback: options.usePostMessageFallback,
        });
        this.tracerProvider = new WebTracerProvider({
            resource: resourceFromAttributes({
                [SemanticAttributes.SEMRESATTRS_SERVICE_NAME]: application,
                [SemanticAttributes.SEMRESATTRS_SERVICE_VERSION]: version,
                [SemanticAttributes.SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: environment,
            }),
            idGenerator: this.idGenerator,
            sampler: new SessionRecorderTraceIdRatioBasedSampler(this.config.sampleTraceRatio),
            spanProcessors: [
                this._getSpanSessionIdProcessor(),
                new BatchSpanProcessor(this.exporter),
            ],
        });
        this.tracerProvider.register({
            // contextManager: new ZoneContextManager(),
            propagator: new W3CTraceContextPropagator(),
        });
        registerInstrumentations({
            tracerProvider: this.tracerProvider,
            instrumentations: [
                getWebAutoInstrumentations({
                    '@opentelemetry/instrumentation-xml-http-request': {
                        clearTimingResources: true,
                        ignoreUrls: [
                            ...OTEL_IGNORE_URLS,
                            ...(this.config.ignoreUrls || []),
                        ],
                        propagateTraceHeaderCorsUrls: options.propagateTraceHeaderCorsUrls,
                        applyCustomAttributesOnSpan: (span, xhr) => {
                            if (!this.config)
                                return;
                            const { captureBody, captureHeaders } = this.config;
                            try {
                                if (!captureBody && !captureHeaders) {
                                    return;
                                }
                                // @ts-ignore
                                const networkRequest = xhr.networkRequest;
                                const requestBody = networkRequest.requestBody;
                                const responseBody = networkRequest.responseBody;
                                const requestHeaders = networkRequest.requestHeaders || {};
                                const responseHeaders = networkRequest.responseHeaders || {};
                                const payload = {
                                    requestBody,
                                    responseBody,
                                    requestHeaders,
                                    responseHeaders,
                                };
                                processHttpPayload(payload, this.config, span);
                            }
                            catch (error) {
                                // eslint-disable-next-line
                                console.error('[MULTIPLAYER_SESSION_RECORDER] Failed to capture xml-http payload', error);
                            }
                        },
                    },
                    '@opentelemetry/instrumentation-fetch': {
                        clearTimingResources: true,
                        ignoreUrls: [
                            ...OTEL_IGNORE_URLS,
                            ...(this.config.ignoreUrls || []),
                        ],
                        propagateTraceHeaderCorsUrls: options.propagateTraceHeaderCorsUrls,
                        applyCustomAttributesOnSpan: async (span, request, response) => {
                            if (!this.config)
                                return;
                            const { captureBody, captureHeaders } = this.config;
                            try {
                                if (!captureBody && !captureHeaders) {
                                    return;
                                }
                                // Try to get data from our fetch wrapper first
                                // @ts-ignore
                                const networkRequest = response === null || response === void 0 ? void 0 : response.networkRequest;
                                let requestBody = null;
                                let responseBody = null;
                                let requestHeaders = {};
                                let responseHeaders = {};
                                if (networkRequest) {
                                    // Use data captured by our fetch wrapper
                                    requestBody = networkRequest.requestBody;
                                    responseBody = networkRequest.responseBody;
                                    requestHeaders = networkRequest.requestHeaders || {};
                                    responseHeaders = networkRequest.responseHeaders || {};
                                }
                                else {
                                    // Fallback to original OpenTelemetry approach
                                    requestBody = request.body;
                                    requestHeaders = headersToObject(request.headers);
                                    responseHeaders = headersToObject(response instanceof Response ? response.headers : undefined);
                                    if (response instanceof Response && response.body) {
                                        responseBody = await extractResponseBody(response);
                                    }
                                }
                                const payload = {
                                    requestBody,
                                    responseBody,
                                    requestHeaders,
                                    responseHeaders,
                                };
                                processHttpPayload(payload, this.config, span);
                            }
                            catch (error) {
                                // eslint-disable-next-line
                                console.error('[MULTIPLAYER_SESSION_RECORDER] Failed to capture fetch payload', error);
                            }
                        },
                    },
                    '@opentelemetry/instrumentation-user-interaction': {
                        shouldPreventSpanCreation: (_event, element, span) => {
                            if (span['parentSpanContext']) {
                                return true;
                            }
                            span.setAttribute('target.innerText', getElementTextContent(element));
                            Array.from(element.attributes).forEach(attribute => {
                                span.setAttribute(`target.attribute.${attribute.name}`, attribute.value);
                            });
                            return false;
                        },
                    },
                }),
            ],
        });
        this._registerGlobalErrorListeners();
    }
    start(sessionId, sessionType) {
        if (!this.tracerProvider) {
            throw new Error('Configuration not initialized. Call init() before start().');
        }
        this.setSessionId(sessionId, sessionType);
    }
    stop() {
        if (!this.tracerProvider) {
            throw new Error('Configuration not initialized. Call init() before start().');
        }
        this.setSessionId('');
    }
    setApiKey(apiKey) {
        if (!this.exporter) {
            throw new Error('Configuration not initialized. Call init() before setApiKey().');
        }
        this.exporter.setApiKey(apiKey);
    }
    /**
     * Capture an exception as an error span/event.
     * If there is an active span, the exception will be recorded on it.
     * Otherwise, a short-lived span will be created to hold the exception event.
     */
    captureException(error, errorInfo) {
        if (!error)
            return;
        // Prefer attaching to the active span to keep correlation intact
        try {
            const activeSpan = trace.getSpan(context.active());
            if (activeSpan) {
                // Standard OTEL exception event + span status
                SessionRecorderSdk.captureException(error);
                activeSpan.addEvent('exception', {
                    'exception.type': error.name || 'Error',
                    'exception.message': error.message,
                    'exception.stacktrace': error.stack || '',
                    ...(errorInfo || {}),
                });
                return;
            }
        }
        catch (_ignored) { }
        // Fallback: create a short-lived span to hold the exception details
        try {
            const tracer = trace.getTracer('exception');
            const span = tracer.startSpan(error.name || 'Error');
            span.recordException(error);
            span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
            span.addEvent('exception', {
                'exception.type': error.name || 'Error',
                'exception.message': error.message,
                'exception.stacktrace': error.stack || '',
                ...(errorInfo || {}),
            });
            span.end();
        }
        catch (_ignored) { }
    }
    _getSpanSessionIdProcessor() {
        return {
            forceFlush: () => Promise.resolve(),
            onEnd: () => { },
            shutdown: () => Promise.resolve(),
            onStart: (span) => {
                var _a;
                if ((_a = this.sessionId) === null || _a === void 0 ? void 0 : _a.length) {
                    span.setAttribute(ATTR_MULTIPLAYER_SESSION_ID, this.sessionId);
                }
            },
        };
    }
    _registerGlobalErrorListeners() {
        if (this.globalErrorListenersRegistered)
            return;
        if (typeof window === 'undefined')
            return;
        const errorHandler = (event) => {
            const err = (event === null || event === void 0 ? void 0 : event.error) instanceof Error
                ? event.error
                : new Error((event === null || event === void 0 ? void 0 : event.message) || 'Script error');
            this.captureException(err);
        };
        const rejectionHandler = (event) => {
            const reason = (event && 'reason' in event) ? event.reason : undefined;
            const err = reason instanceof Error
                ? reason
                : new Error(typeof reason === 'string' ? reason : 'Unhandled promise rejection');
            this.captureException(err);
        };
        window.addEventListener('error', errorHandler);
        window.addEventListener('unhandledrejection', rejectionHandler);
        this.globalErrorListenersRegistered = true;
    }
}
//# sourceMappingURL=index.js.map