import { getClient, getCurrentScope, captureEvent } from '@sentry/core';
import { SentryError, logger, parseEnvelope, forEachEnvelopeItem } from '@sentry/utils';
import { app, protocol, ipcMain, webContents } from 'electron';
import { TextEncoder, TextDecoder } from 'util';
import { createRendererAnrStatusHandler } from './anr.js';
import { supportsFullProtocol, whenAppReady, registerProtocol } from './electron-normalize.js';
import { rendererProfileFromIpc } from './integrations/renderer-profiling.js';
import { IPCChannel, PROTOCOL_SCHEME } from '../common/ipc.js';
import { normalizeUrlsInReplayEnvelope } from '../common/normalize.js';
import { IPCMode } from '../common/mode.js';
import { mergeEvents } from '../common/merge.js';

let KNOWN_RENDERERS;
let WINDOW_ID_TO_WEB_CONTENTS;
const SENTRY_CUSTOM_SCHEME = {
    scheme: PROTOCOL_SCHEME,
    privileges: { bypassCSP: true, corsEnabled: true, supportFetchAPI: true, secure: true },
};
function newProtocolRenderer() {
    KNOWN_RENDERERS = KNOWN_RENDERERS || new Set();
    WINDOW_ID_TO_WEB_CONTENTS = WINDOW_ID_TO_WEB_CONTENTS || new Map();
    for (const wc of webContents.getAllWebContents()) {
        const wcId = wc.id;
        if (KNOWN_RENDERERS.has(wcId)) {
            continue;
        }
        if (!wc.isDestroyed()) {
            wc.executeJavaScript('window.__SENTRY_RENDERER_ID__').then((windowId) => {
                if (windowId && KNOWN_RENDERERS && WINDOW_ID_TO_WEB_CONTENTS) {
                    KNOWN_RENDERERS.add(wcId);
                    WINDOW_ID_TO_WEB_CONTENTS.set(windowId, wcId);
                    wc.once('destroyed', () => {
                        KNOWN_RENDERERS === null || KNOWN_RENDERERS === void 0 ? void 0 : KNOWN_RENDERERS.delete(wcId);
                        WINDOW_ID_TO_WEB_CONTENTS === null || WINDOW_ID_TO_WEB_CONTENTS === void 0 ? void 0 : WINDOW_ID_TO_WEB_CONTENTS.delete(windowId);
                    });
                }
            }, logger.error);
        }
    }
}
function captureEventFromRenderer(options, event, attachments, contents) {
    var _a, _b, _c, _d;
    const process = contents ? ((_a = options === null || options === void 0 ? void 0 : options.getRendererName) === null || _a === void 0 ? void 0 : _a.call(options, contents)) || 'renderer' : 'renderer';
    // Ensure breadcrumbs are empty as they sent via scope updates
    event.breadcrumbs = event.breadcrumbs || [];
    // Remove the environment as it defaults to 'production' and overwrites the main process environment
    delete event.environment;
    // Remove the SDK info as we want the Electron SDK to be the one reporting the event
    (_b = event.sdk) === null || _b === void 0 ? true : delete _b.name;
    (_c = event.sdk) === null || _c === void 0 ? true : delete _c.version;
    (_d = event.sdk) === null || _d === void 0 ? true : delete _d.packages;
    captureEvent(mergeEvents(event, { tags: { 'event.process': process } }), { attachments });
}
function handleEvent(options, jsonEvent, contents) {
    let event;
    try {
        event = JSON.parse(jsonEvent);
    }
    catch (_a) {
        logger.warn('sentry-electron received an invalid event message');
        return;
    }
    captureEventFromRenderer(options, event, [], contents);
}
function eventFromEnvelope(envelope) {
    let event;
    const attachments = [];
    let profile;
    forEachEnvelopeItem(envelope, (item, type) => {
        if (type === 'event' || type === 'transaction' || type === 'feedback') {
            event = Array.isArray(item) ? item[1] : undefined;
        }
        else if (type === 'attachment') {
            const [headers, data] = item;
            attachments.push({
                filename: headers.filename,
                attachmentType: headers.attachment_type,
                contentType: headers.content_type,
                data,
            });
        }
        else if (type === 'profile') {
            profile = item[1];
        }
    });
    return event ? [event, attachments, profile] : undefined;
}
function handleEnvelope(options, env, contents) {
    var _a, _b;
    const envelope = parseEnvelope(env, new TextEncoder(), new TextDecoder());
    const eventAndAttachments = eventFromEnvelope(envelope);
    if (eventAndAttachments) {
        const [event, attachments, profile] = eventAndAttachments;
        if (profile) {
            // We have a 'profile' item and there is no way for us to pass this through event capture
            // so store them in a cache and reattach them via the `beforeEnvelope` hook before sending
            rendererProfileFromIpc(event, profile);
        }
        captureEventFromRenderer(options, event, attachments, contents);
    }
    else {
        const normalizedEnvelope = normalizeUrlsInReplayEnvelope(envelope, app.getAppPath());
        // Pass other types of envelope straight to the transport
        void ((_b = (_a = getClient()) === null || _a === void 0 ? void 0 : _a.getTransport()) === null || _b === void 0 ? void 0 : _b.send(normalizedEnvelope));
    }
}
function handleMetric(metric) {
    const client = getClient();
    if (client === null || client === void 0 ? void 0 : client.metricsAggregator) {
        client.metricsAggregator.add(metric.metricType, metric.name, metric.value, metric.unit, metric.tags, metric.timestamp);
    }
    else {
        logger.warn(`Metric was dropped because the aggregator is not configured in the main process. Enable via '_experiments.metricsAggregator: true' in your init call.`);
    }
}
/** Is object defined and has keys */
function hasKeys(obj) {
    return obj != undefined && Object.keys(obj).length > 0;
}
/**
 * Handle scope updates from renderer processes
 */
function handleScope(options, jsonScope) {
    let sentScope;
    try {
        sentScope = JSON.parse(jsonScope);
    }
    catch (_a) {
        logger.warn('sentry-electron received an invalid scope message');
        return;
    }
    const scope = getCurrentScope();
    if (hasKeys(sentScope.user)) {
        scope.setUser(sentScope.user);
    }
    if (hasKeys(sentScope.tags)) {
        scope.setTags(sentScope.tags);
    }
    if (hasKeys(sentScope.extra)) {
        scope.setExtras(sentScope.extra);
    }
    for (const attachment of sentScope.attachments || []) {
        scope.addAttachment(attachment);
    }
    const breadcrumb = sentScope.breadcrumbs.pop();
    if (breadcrumb) {
        scope.addBreadcrumb(breadcrumb, (options === null || options === void 0 ? void 0 : options.maxBreadcrumbs) || 100);
    }
}
/** Enables Electron protocol handling */
function configureProtocol(options) {
    if (app.isReady()) {
        throw new SentryError("Sentry SDK should be initialized before the Electron app 'ready' event is fired");
    }
    protocol.registerSchemesAsPrivileged([SENTRY_CUSTOM_SCHEME]);
    // We Proxy this function so that later user calls to registerSchemesAsPrivileged don't overwrite our custom scheme
    // eslint-disable-next-line @typescript-eslint/unbound-method
    protocol.registerSchemesAsPrivileged = new Proxy(protocol.registerSchemesAsPrivileged, {
        apply: (target, __, args) => {
            target([...args[0], SENTRY_CUSTOM_SCHEME]);
        },
    });
    const rendererStatusChanged = createRendererAnrStatusHandler();
    whenAppReady
        .then(() => {
        for (const sesh of options.getSessions()) {
            registerProtocol(sesh.protocol, PROTOCOL_SCHEME, (request) => {
                const getWebContents = () => {
                    const webContentsId = request.windowId ? WINDOW_ID_TO_WEB_CONTENTS === null || WINDOW_ID_TO_WEB_CONTENTS === void 0 ? void 0 : WINDOW_ID_TO_WEB_CONTENTS.get(request.windowId) : undefined;
                    return webContentsId ? webContents.fromId(webContentsId) : undefined;
                };
                const data = request.body;
                if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.RENDERER_START}`)) {
                    newProtocolRenderer();
                }
                else if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.EVENT}`) && data) {
                    handleEvent(options, data.toString(), getWebContents());
                }
                else if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.SCOPE}`) && data) {
                    handleScope(options, data.toString());
                }
                else if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.ENVELOPE}`) && data) {
                    handleEnvelope(options, data, getWebContents());
                }
                else if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.ADD_METRIC}`) && data) {
                    handleMetric(JSON.parse(data.toString()));
                }
                else if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.STATUS}`) && data) {
                    const contents = getWebContents();
                    if (contents) {
                        const status = JSON.parse(data.toString()).status;
                        rendererStatusChanged(status, contents);
                    }
                }
            });
        }
    })
        .catch((error) => logger.error(error));
}
/**
 * Hooks IPC for communication with the renderer processes
 */
function configureClassic(options) {
    ipcMain.on(IPCChannel.RENDERER_START, ({ sender }) => {
        const id = sender.id;
        // Keep track of renderers that are using IPC
        KNOWN_RENDERERS = KNOWN_RENDERERS || new Set();
        if (KNOWN_RENDERERS.has(id)) {
            return;
        }
        // In older Electron, sender can be destroyed before this callback is called
        if (!sender.isDestroyed()) {
            KNOWN_RENDERERS.add(id);
            sender.once('destroyed', () => {
                KNOWN_RENDERERS === null || KNOWN_RENDERERS === void 0 ? void 0 : KNOWN_RENDERERS.delete(id);
            });
        }
    });
    ipcMain.on(IPCChannel.EVENT, ({ sender }, jsonEvent) => handleEvent(options, jsonEvent, sender));
    ipcMain.on(IPCChannel.SCOPE, (_, jsonScope) => handleScope(options, jsonScope));
    ipcMain.on(IPCChannel.ENVELOPE, ({ sender }, env) => handleEnvelope(options, env, sender));
    const rendererStatusChanged = createRendererAnrStatusHandler();
    ipcMain.on(IPCChannel.STATUS, ({ sender }, status) => rendererStatusChanged(status, sender));
    ipcMain.on(IPCChannel.ADD_METRIC, (_, metric) => handleMetric(metric));
}
/** Sets up communication channels with the renderer */
function configureIPC(options) {
    if (!supportsFullProtocol() && options.ipcMode === IPCMode.Protocol) {
        throw new SentryError('IPCMode.Protocol is only supported in Electron >= v5');
    }
    // eslint-disable-next-line no-bitwise
    if (supportsFullProtocol() && (options.ipcMode & IPCMode.Protocol) > 0) {
        configureProtocol(options);
    }
    // eslint-disable-next-line no-bitwise
    if ((options.ipcMode & IPCMode.Classic) > 0) {
        configureClassic(options);
    }
}

export { configureIPC };
//# sourceMappingURL=ipc.js.map
