const core = require('@sentry/core');
const node = require('@sentry/node');
const electron = require('electron');
const electronNormalize = require('../electron-normalize.js');
const headerInjection = require('../header-injection.js');
const sessions = require('../sessions.js');
const stackParse = require('../stack-parse.js');

function log(message, ...args) {
    core.debug.log(`[Renderer Event Loop Block] ${message}`, ...args);
}
/**
 * Captures stack frames via the native frame.collectJavaScriptCallStack() API
 */
function nativeStackTraceCapture(contents, pausedStack) {
    return () => {
        stackParse.captureRendererStackFrames(contents)
            .then((frames) => {
            if (frames) {
                pausedStack(frames);
            }
        })
            .catch(() => {
            // ignore
        });
    };
}
/**
 * Captures stack frames via the debugger
 */
function debuggerStackTraceCapture(contents, pausedStack) {
    log('Connecting to debugger');
    contents.debugger.attach('1.3');
    // Collect scriptId -> url map so we can look up the filenames later
    const scripts = new Map();
    const getModuleFromFilename = node.createGetModuleFromFilename(electron.app.getAppPath());
    contents.debugger.on('message', (_, method, params) => {
        if (method === 'Debugger.scriptParsed') {
            const param = params;
            scripts.set(param.scriptId, param.url);
        }
        else if (method === 'Debugger.paused') {
            const param = params;
            if (param.reason !== 'other') {
                return;
            }
            // copy the frames
            const callFrames = [...param.callFrames];
            contents.debugger.sendCommand('Debugger.resume').then(null, () => {
                // ignore
            });
            const stackFrames = core.stripSentryFramesAndReverse(callFrames.map((frame) => core.callFrameToStackFrame(frame, scripts.get(frame.location.scriptId), getModuleFromFilename)));
            pausedStack(stackFrames);
        }
    });
    // In node, we enable just before pausing but for Chrome, the debugger must be enabled before he ANR event occurs
    contents.debugger.sendCommand('Debugger.enable').catch(() => {
        // ignore
    });
    return () => {
        if (contents.isDestroyed()) {
            return;
        }
        log('Pausing debugger to capture stack trace');
        return contents.debugger.sendCommand('Debugger.pause');
    };
}
function createHrTimer() {
    let lastPoll = process.hrtime();
    return {
        getTimeMs: () => {
            const [seconds, nanoSeconds] = process.hrtime(lastPoll);
            return Math.floor(seconds * 1e3 + nanoSeconds / 1e6);
        },
        reset: () => {
            lastPoll = process.hrtime();
        },
    };
}
const INTEGRATION_NAME = 'RendererEventLoopBlock';
/**
 * An integration that captures App Not Responding events from renderer processes
 */
const rendererEventLoopBlockIntegration = core.defineIntegration((options = {}) => {
    const rendererWatchdogTimers = new Map();
    let clientOptions;
    function getRendererName(contents) {
        return clientOptions?.getRendererName?.(contents);
    }
    function sendRendererEventLoopBlockEvent(contents, blockedMs, frames) {
        sessions.sessionAnr();
        const rendererName = getRendererName(contents) || 'renderer';
        const event = {
            level: 'error',
            exception: {
                values: [
                    {
                        type: 'ApplicationNotResponding',
                        value: `Application Not Responding for at least ${blockedMs} ms`,
                        stacktrace: { frames },
                        mechanism: {
                            // This ensures the UI doesn't say 'Crashed in' for the stack trace
                            type: 'ANR',
                        },
                    },
                ],
            },
            tags: {
                'event.process': rendererName,
            },
        };
        core.captureEvent(event);
    }
    return {
        name: INTEGRATION_NAME,
        setup: (client) => {
            clientOptions = client.getOptions();
            if (electronNormalize.ELECTRON_MAJOR_VERSION >= 34) {
                electron.app.commandLine.appendSwitch('enable-features', 'DocumentPolicyIncludeJSCallStacksInCrashReports');
                if (options.captureNativeStacktrace) {
                    electron.app.on('ready', () => {
                        clientOptions
                            ?.getSessions()
                            .forEach((sesh) => headerInjection.addHeaderToSession(sesh, 'Document-Policy', 'include-js-call-stacks-in-crash-reports'));
                    });
                }
            }
        },
        createRendererEventLoopBlockStatusHandler: () => {
            return (message, contents) => {
                let watchdog = rendererWatchdogTimers.get(contents);
                // eslint-disable-next-line jsdoc/require-jsdoc
                function disable() {
                    watchdog?.enabled(false);
                }
                // eslint-disable-next-line jsdoc/require-jsdoc
                function enable() {
                    watchdog?.enabled(true);
                }
                if (watchdog === undefined) {
                    log('Renderer sent first status message', message.config);
                    let pauseAndCapture;
                    if (message.config.captureStackTrace) {
                        const stackCaptureImpl = options.captureNativeStacktrace && electronNormalize.ELECTRON_MAJOR_VERSION >= 34
                            ? nativeStackTraceCapture
                            : debuggerStackTraceCapture;
                        pauseAndCapture = stackCaptureImpl(contents, (frames) => {
                            log('Event captured with stack frames');
                            sendRendererEventLoopBlockEvent(contents, message.config.anrThreshold, frames);
                        });
                    }
                    watchdog = core.watchdogTimer(createHrTimer, 100, message.config.anrThreshold, async () => {
                        log('Watchdog timeout');
                        if (pauseAndCapture) {
                            pauseAndCapture();
                        }
                        else {
                            log('Capturing event');
                            sendRendererEventLoopBlockEvent(contents, message.config.anrThreshold);
                        }
                    });
                    contents.once('destroyed', () => {
                        rendererWatchdogTimers?.delete(contents);
                        electron.powerMonitor.off('suspend', disable);
                        electron.powerMonitor.off('resume', enable);
                        electron.powerMonitor.off('lock-screen', disable);
                        electron.powerMonitor.off('unlock-screen', enable);
                    });
                    contents.once('blur', disable);
                    contents.once('focus', enable);
                    electron.powerMonitor.on('suspend', disable);
                    electron.powerMonitor.on('resume', enable);
                    electron.powerMonitor.on('lock-screen', disable);
                    electron.powerMonitor.on('unlock-screen', enable);
                    rendererWatchdogTimers.set(contents, watchdog);
                }
                watchdog.poll();
                if (message.status !== 'alive') {
                    log(`Renderer visibility changed '${message.status}'`);
                    watchdog.enabled(message.status === 'visible');
                }
            };
        },
    };
});
/**
 * Creates a hook which notifies the integration when the state of renderers change
 */
function createRendererEventLoopBlockStatusHandler(client) {
    const integration = client.getIntegrationByName(INTEGRATION_NAME);
    return integration?.createRendererEventLoopBlockStatusHandler();
}

exports.createRendererEventLoopBlockStatusHandler = createRendererEventLoopBlockStatusHandler;
exports.rendererEventLoopBlockIntegration = rendererEventLoopBlockIntegration;
//# sourceMappingURL=renderer-anr.js.map
