import { addBreadcrumb, debug, getClient, isPlainObject, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_OK, spanToJSON, startInactiveSpan, timestampInSeconds, } from '@sentry/core';
import { getAppRegistryIntegration } from '../integrations/appRegistry';
import { isSentrySpan } from '../utils/span';
import { RN_GLOBAL_OBJ } from '../utils/worldwide';
import { NATIVE } from '../wrapper';
import { ignoreEmptyBackNavigation, ignoreEmptyRouteChangeTransactions } from './onSpanEndUtils';
import { SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NAVIGATION } from './origin';
import { getReactNativeTracingIntegration } from './reactnativetracing';
import { SEMANTIC_ATTRIBUTE_NAVIGATION_ACTION_TYPE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from './semanticAttributes';
import { DEFAULT_NAVIGATION_SPAN_NAME, defaultIdleOptions, getDefaultIdleNavigationSpanOptions, startIdleNavigationSpan as startGenericIdleNavigationSpan, } from './span';
import { addTimeToInitialDisplayFallback } from './timeToDisplayFallback';
export const INTEGRATION_NAME = 'ReactNavigation';
const NAVIGATION_HISTORY_MAX_SIZE = 200;
/**
 * Extracts dynamic route parameters from a route name and its params.
 * Matches Expo Router style dynamic segments like `[id]` and `[...slug]`.
 *
 * Only params whose keys appear as dynamic segments in the route name are returned,
 * filtering out non-structural params (query params, etc.) that may contain PII.
 *
 * Note: dynamic segment values (e.g. the `123` in `profile/[id]`) may be user-identifiable.
 * This function only extracts params — callers are responsible for checking `sendDefaultPii`
 * before including the result in span attributes.
 *
 * Previous route params are intentionally not captured — only the current route's
 * structural params are needed for trace attribution.
 */
export function extractDynamicRouteParams(routeName, params) {
    if (!params) {
        return undefined;
    }
    const dynamicKeys = new Set();
    const pattern = /\[(?:\.\.\.)?(\w+)\]/g;
    let match;
    while ((match = pattern.exec(routeName)) !== null) {
        if (match[1]) {
            dynamicKeys.add(match[1]);
        }
    }
    if (dynamicKeys.size === 0) {
        return undefined;
    }
    const result = {};
    for (const key of dynamicKeys) {
        if (key in params) {
            const value = params[key];
            result[`route.params.${key}`] = Array.isArray(value) ? value.join('/') : String(value !== null && value !== void 0 ? value : '');
        }
    }
    return Object.keys(result).length > 0 ? result : undefined;
}
/**
 * Builds a full path from the navigation state by traversing nested navigators.
 * For example, with nested navigators: "Home/Settings/Profile"
 */
function getPathFromState(state) {
    var _a;
    if (!state) {
        return undefined;
    }
    const routeNames = [];
    let currentState = state;
    while (currentState) {
        const index = (_a = currentState.index) !== null && _a !== void 0 ? _a : 0;
        const route = currentState.routes[index];
        if (route === null || route === void 0 ? void 0 : route.name) {
            routeNames.push(route.name);
        }
        currentState = route === null || route === void 0 ? void 0 : route.state;
    }
    return routeNames.length > 0 ? routeNames.join('/') : undefined;
}
/**
 * Instrumentation for React-Navigation V5 and above. See docs or sample app for usage.
 *
 * How this works:
 * - `_onDispatch` is called every time a dispatch happens and sets an IdleTransaction on the scope without any route context.
 * - `_onStateChange` is then called AFTER the state change happens due to a dispatch and sets the route context onto the active transaction.
 * - If `_onStateChange` isn't called within `STATE_CHANGE_TIMEOUT_DURATION` of the dispatch, then the transaction is not sampled and finished.
 */
export const reactNavigationIntegration = ({ routeChangeTimeoutMs = 1000, enableTimeToInitialDisplay = false, ignoreEmptyBackNavigationTransactions = true, enableTimeToInitialDisplayForPreloadedRoutes = false, useDispatchedActionData = false, useFullPathsForNavigationRoutes = false, enablePrefetchTracking = false, } = {}) => {
    let navigationContainer;
    let tracing;
    let idleSpanOptions = defaultIdleOptions;
    let latestRoute;
    let latestNavigationSpan;
    let navigationProcessingSpan;
    let initialStateHandled = false;
    let isSetupComplete = false;
    let stateChangeTimeout;
    let recentRouteKeys = [];
    if (enableTimeToInitialDisplay) {
        NATIVE.initNativeReactNavigationNewFrameTracking().catch((reason) => {
            debug.error(`${INTEGRATION_NAME} Failed to initialize native new frame tracking: ${reason}`);
        });
    }
    /**
     * Set the initial state and start initial navigation span for the current screen.
     */
    const afterAllSetup = (client) => {
        var _a;
        tracing = getReactNativeTracingIntegration(client);
        if (tracing) {
            idleSpanOptions = {
                finalTimeout: tracing.options.finalTimeoutMs,
                idleTimeout: tracing.options.idleTimeoutMs,
            };
        }
        if (initialStateHandled) {
            // We create an initial state here to ensure a transaction gets created before the first route mounts.
            // This assumes that the Sentry.init() call is made before the first route mounts.
            // If this is not the case, the first transaction will be nameless 'Route Changed'
            return undefined;
        }
        (_a = getAppRegistryIntegration(client)) === null || _a === void 0 ? void 0 : _a.onRunApplication(() => {
            if (initialStateHandled) {
                // To avoid conflict with the initial transaction we check if it was already handled.
                // This ensures runApplication calls after the initial start are correctly traced.
                // This is used for example when Activity is (re)started on Android.
                debug.log('[ReactNavigationIntegration] Starting new idle navigation span based on runApplication call.');
                startIdleNavigationSpan(undefined, true);
            }
        });
        isSetupComplete = true;
        if (!navigationContainer) {
            // This is expected as navigation container is registered after the root component is mounted.
            return undefined;
        }
        // Navigation container already registered, create and populate initial span
        startIdleNavigationSpan();
        updateLatestNavigationSpanWithCurrentRoute();
        initialStateHandled = true;
    };
    const registerNavigationContainer = (maybeNewNavigationContainer) => {
        if (RN_GLOBAL_OBJ.__sentry_rn_v5_registered) {
            debug.log(`${INTEGRATION_NAME} Instrumentation already exists, but registering again...`);
            // In the past we have not allowed re-registering the navigation container to avoid unexpected behavior.
            // But this doesn't work for Android and re-recreating application main activity.
            // Where new navigation container is created and the old one is discarded. We need to re-register to
            // trace the new navigation container navigation.
        }
        let newNavigationContainer;
        if (isPlainObject(maybeNewNavigationContainer) && 'current' in maybeNewNavigationContainer) {
            newNavigationContainer = maybeNewNavigationContainer.current;
        }
        else {
            newNavigationContainer = maybeNewNavigationContainer;
        }
        if (navigationContainer === newNavigationContainer) {
            debug.log(`${INTEGRATION_NAME} Navigation container ref is the same as the one already registered.`);
            return;
        }
        navigationContainer = newNavigationContainer;
        if (!navigationContainer) {
            debug.warn(`${INTEGRATION_NAME} Received invalid navigation container ref!`);
            return undefined;
        }
        // This action is emitted on every dispatch
        navigationContainer.addListener('__unsafe_action__', startIdleNavigationSpan);
        navigationContainer.addListener('state', updateLatestNavigationSpanWithCurrentRoute);
        RN_GLOBAL_OBJ.__sentry_rn_v5_registered = true;
        if (initialStateHandled) {
            return undefined;
        }
        if (!latestNavigationSpan) {
            if (!isSetupComplete) {
                debug.log(`${INTEGRATION_NAME} Navigation container registered before integration setup. Initial span will be created when setup completes.`);
                return undefined;
            }
            startIdleNavigationSpan();
        }
        // Navigation Container is registered after the first navigation
        // Initial navigation span was started, after integration setup,
        // so now we populate it with the current route.
        updateLatestNavigationSpanWithCurrentRoute();
        initialStateHandled = true;
    };
    /**
     * To be called on every React-Navigation action dispatch.
     * It does not name the transaction or populate it with route information. Instead, it waits for the state to fully change
     * and gets the route information from there, @see updateLatestNavigationSpanWithCurrentRoute
     *
     * @param unknownEvent - The event object that contains navigation action data
     * @param isAppRestart - Whether this span is being started due to an app restart rather than a normal navigation action
     */
    // eslint-disable-next-line complexity
    const startIdleNavigationSpan = (unknownEvent, isAppRestart = false) => {
        const event = unknownEvent;
        if (useDispatchedActionData && (event === null || event === void 0 ? void 0 : event.data.noop)) {
            debug.log(`${INTEGRATION_NAME} Navigation action is a noop, not starting navigation span.`);
            return;
        }
        const navigationActionType = useDispatchedActionData ? event === null || event === void 0 ? void 0 : event.data.action.type : undefined;
        // Handle PRELOAD actions separately if prefetch tracking is enabled
        if (enablePrefetchTracking && navigationActionType === 'PRELOAD') {
            const preloadData = event === null || event === void 0 ? void 0 : event.data.action;
            const payload = preloadData === null || preloadData === void 0 ? void 0 : preloadData.payload;
            const targetRoute = payload && typeof payload === 'object' && 'name' in payload && typeof payload.name === 'string'
                ? payload.name
                : 'Unknown Route';
            debug.log(`${INTEGRATION_NAME} Starting prefetch span for route: ${targetRoute}`);
            const prefetchSpan = startInactiveSpan({
                op: 'navigation.prefetch',
                name: `Prefetch ${targetRoute}`,
                attributes: {
                    'route.name': targetRoute,
                },
            });
            // Store prefetch span to end it when state changes or timeout
            navigationProcessingSpan = prefetchSpan;
            // Set timeout to ensure we don't leave hanging spans
            stateChangeTimeout = setTimeout(() => {
                if (navigationProcessingSpan === prefetchSpan) {
                    debug.log(`${INTEGRATION_NAME} Prefetch span timed out for route: ${targetRoute}`);
                    prefetchSpan === null || prefetchSpan === void 0 ? void 0 : prefetchSpan.setStatus({ code: SPAN_STATUS_OK });
                    prefetchSpan === null || prefetchSpan === void 0 ? void 0 : prefetchSpan.end();
                    navigationProcessingSpan = undefined;
                }
            }, routeChangeTimeoutMs);
            return;
        }
        if (useDispatchedActionData &&
            navigationActionType &&
            [
                // Process common actions
                'PRELOAD', // Still filter PRELOAD when enablePrefetchTracking is false
                'SET_PARAMS',
                // Drawer actions
                'OPEN_DRAWER',
                'CLOSE_DRAWER',
                'TOGGLE_DRAWER',
            ].includes(navigationActionType)) {
            debug.log(`${INTEGRATION_NAME} Navigation action is ${navigationActionType}, not starting navigation span.`);
            return;
        }
        if (latestNavigationSpan) {
            debug.log(`${INTEGRATION_NAME} A transaction was detected that turned out to be a noop, discarding.`);
            _discardLatestTransaction();
            clearStateChangeTimeout();
        }
        latestNavigationSpan = startGenericIdleNavigationSpan((tracing === null || tracing === void 0 ? void 0 : tracing.options.beforeStartSpan)
            ? tracing.options.beforeStartSpan(getDefaultIdleNavigationSpanOptions())
            : getDefaultIdleNavigationSpanOptions(), Object.assign(Object.assign({}, idleSpanOptions), { isAppRestart }));
        latestNavigationSpan === null || latestNavigationSpan === void 0 ? void 0 : latestNavigationSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NAVIGATION);
        latestNavigationSpan === null || latestNavigationSpan === void 0 ? void 0 : latestNavigationSpan.setAttribute(SEMANTIC_ATTRIBUTE_NAVIGATION_ACTION_TYPE, navigationActionType);
        if (ignoreEmptyBackNavigationTransactions) {
            ignoreEmptyBackNavigation(getClient(), latestNavigationSpan);
        }
        // Always discard transactions that never receive route information
        const spanToCheck = latestNavigationSpan;
        ignoreEmptyRouteChangeTransactions(getClient(), spanToCheck, DEFAULT_NAVIGATION_SPAN_NAME, () => latestNavigationSpan === spanToCheck);
        if (enableTimeToInitialDisplay && latestNavigationSpan) {
            NATIVE.setActiveSpanId(latestNavigationSpan.spanContext().spanId);
            navigationProcessingSpan = startInactiveSpan({
                op: 'navigation.processing',
                name: 'Navigation dispatch to navigation cancelled or screen mounted',
                startTime: spanToJSON(latestNavigationSpan).start_timestamp,
            });
            navigationProcessingSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NAVIGATION);
        }
        stateChangeTimeout = setTimeout(_discardLatestTransaction, routeChangeTimeoutMs);
    };
    /**
     * To be called AFTER the state has been changed to populate the transaction with the current route.
     */
    // eslint-disable-next-line complexity
    const updateLatestNavigationSpanWithCurrentRoute = () => {
        var _a, _b, _c;
        const stateChangedTimestamp = timestampInSeconds();
        const previousRoute = latestRoute;
        if (!navigationContainer) {
            debug.warn(`${INTEGRATION_NAME} Missing navigation container ref. Route transactions will not be sent.`);
            return undefined;
        }
        const route = navigationContainer.getCurrentRoute();
        if (!route) {
            debug.log(`[${INTEGRATION_NAME}] Navigation state changed, but no route is rendered.`);
            return undefined;
        }
        if (!latestNavigationSpan) {
            debug.log(`[${INTEGRATION_NAME}] Navigation state changed, but navigation transaction was not started on dispatch.`);
            return undefined;
        }
        if (enableTimeToInitialDisplay) {
            addTimeToInitialDisplayFallback(latestNavigationSpan.spanContext().spanId, NATIVE.getNewScreenTimeToDisplay());
        }
        if ((previousRoute === null || previousRoute === void 0 ? void 0 : previousRoute.key) === route.key) {
            debug.log(`[${INTEGRATION_NAME}] Navigation state changed, but route is the same as previous.`);
            pushRecentRouteKey(route.key);
            latestRoute = route;
            // Clear the latest transaction as it has been handled.
            latestNavigationSpan = undefined;
            return undefined;
        }
        const routeHasBeenSeen = recentRouteKeys.includes(route.key);
        // Get the full navigation path for nested navigators
        let routeName = route.name;
        if (useFullPathsForNavigationRoutes) {
            const navigationState = navigationContainer.getState();
            routeName = getPathFromState(navigationState) || route.name;
        }
        navigationProcessingSpan === null || navigationProcessingSpan === void 0 ? void 0 : navigationProcessingSpan.updateName(`Navigation dispatch to screen ${routeName} mounted`);
        navigationProcessingSpan === null || navigationProcessingSpan === void 0 ? void 0 : navigationProcessingSpan.setStatus({ code: SPAN_STATUS_OK });
        navigationProcessingSpan === null || navigationProcessingSpan === void 0 ? void 0 : navigationProcessingSpan.end(stateChangedTimestamp);
        navigationProcessingSpan = undefined;
        if (spanToJSON(latestNavigationSpan).description === DEFAULT_NAVIGATION_SPAN_NAME) {
            latestNavigationSpan.updateName(routeName);
        }
        const sendDefaultPii = (_c = (_b = (_a = getClient()) === null || _a === void 0 ? void 0 : _a.getOptions()) === null || _b === void 0 ? void 0 : _b.sendDefaultPii) !== null && _c !== void 0 ? _c : false;
        latestNavigationSpan.setAttributes(Object.assign(Object.assign({ 'route.name': routeName, 'route.key': route.key }, (sendDefaultPii ? extractDynamicRouteParams(routeName, route.params) : undefined)), { 'route.has_been_seen': routeHasBeenSeen, 'previous_route.name': previousRoute === null || previousRoute === void 0 ? void 0 : previousRoute.name, 'previous_route.key': previousRoute === null || previousRoute === void 0 ? void 0 : previousRoute.key, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation' }));
        // Clear the timeout so the transaction does not get cancelled.
        clearStateChangeTimeout();
        addBreadcrumb({
            category: 'navigation',
            type: 'navigation',
            message: `Navigation to ${routeName}`,
            data: {
                from: previousRoute === null || previousRoute === void 0 ? void 0 : previousRoute.name,
                to: routeName,
            },
        });
        tracing === null || tracing === void 0 ? void 0 : tracing.setCurrentRoute(routeName);
        pushRecentRouteKey(route.key);
        if (useFullPathsForNavigationRoutes) {
            latestRoute = Object.assign(Object.assign({}, route), { name: routeName });
        }
        else {
            latestRoute = route;
        }
        // Clear the latest transaction as it has been handled.
        latestNavigationSpan = undefined;
    };
    /** Pushes a recent route key, and removes earlier routes when there is greater than the max length */
    const pushRecentRouteKey = (key) => {
        recentRouteKeys.push(key);
        if (recentRouteKeys.length > NAVIGATION_HISTORY_MAX_SIZE) {
            recentRouteKeys = recentRouteKeys.slice(recentRouteKeys.length - NAVIGATION_HISTORY_MAX_SIZE);
        }
    };
    /** Cancels the latest transaction so it does not get sent to Sentry. */
    const _discardLatestTransaction = () => {
        if (latestNavigationSpan) {
            if (isSentrySpan(latestNavigationSpan)) {
                latestNavigationSpan['_sampled'] = false;
            }
            // TODO: What if it's not SentrySpan?
            latestNavigationSpan.end();
            latestNavigationSpan = undefined;
        }
        if (navigationProcessingSpan) {
            navigationProcessingSpan = undefined;
        }
    };
    const clearStateChangeTimeout = () => {
        if (typeof stateChangeTimeout !== 'undefined') {
            clearTimeout(stateChangeTimeout);
            stateChangeTimeout = undefined;
        }
    };
    return {
        name: INTEGRATION_NAME,
        afterAllSetup,
        registerNavigationContainer,
        options: {
            routeChangeTimeoutMs,
            enableTimeToInitialDisplay,
            ignoreEmptyBackNavigationTransactions,
            enableTimeToInitialDisplayForPreloadedRoutes,
            useDispatchedActionData,
            useFullPathsForNavigationRoutes,
            enablePrefetchTracking,
        },
    };
};
/**
 * Returns React Navigation integration of the given client.
 */
export function getReactNavigationIntegration(client) {
    return client.getIntegrationByName(INTEGRATION_NAME);
}
//# sourceMappingURL=reactnavigation.js.map