/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
/* eslint-disable @typescript-eslint/no-this-alias */
import http from 'node:http';
import https from 'node:https';
import zlib from 'node:zlib';
import fs from 'node:fs';
import path from 'node:path';
import { EventEmitter } from 'node:events';
import { CoreEvent, coreEvents, debugLogger } from '@google/gemini-cli-core';
const ACTIVITY_ID_HEADER = 'x-activity-request-id';
/**
 * Capture utility for session activities (network and console).
 * Provides a stream of events that can be persisted for analysis or inspection.
 */
export class ActivityLogger extends EventEmitter {
    static instance;
    isInterceptionEnabled = false;
    requestStartTimes = new Map();
    static getInstance() {
        if (!ActivityLogger.instance) {
            ActivityLogger.instance = new ActivityLogger();
        }
        return ActivityLogger.instance;
    }
    stringifyHeaders(headers) {
        const result = {};
        if (!headers)
            return result;
        if (headers instanceof Headers) {
            headers.forEach((v, k) => {
                result[k.toLowerCase()] = v;
            });
        }
        else if (typeof headers === 'object' && headers !== null) {
            for (const [key, val] of Object.entries(headers)) {
                result[key.toLowerCase()] = Array.isArray(val)
                    ? val.join(', ')
                    : String(val);
            }
        }
        return result;
    }
    sanitizeNetworkLog(log) {
        if (!log || typeof log !== 'object')
            return log;
        const sanitized = { ...log };
        // Sanitize request headers
        if (sanitized.headers) {
            const headers = { ...sanitized.headers };
            for (const key of Object.keys(headers)) {
                if (['authorization', 'cookie', 'x-goog-api-key'].includes(key.toLowerCase())) {
                    headers[key] = '[REDACTED]';
                }
            }
            sanitized.headers = headers;
        }
        // Sanitize response headers
        if (sanitized.response?.headers) {
            const resHeaders = { ...sanitized.response.headers };
            for (const key of Object.keys(resHeaders)) {
                if (['set-cookie'].includes(key.toLowerCase())) {
                    resHeaders[key] = '[REDACTED]';
                }
            }
            sanitized.response = { ...sanitized.response, headers: resHeaders };
        }
        return sanitized;
    }
    safeEmitNetwork(payload) {
        this.emit('network', this.sanitizeNetworkLog(payload));
    }
    enable() {
        if (this.isInterceptionEnabled)
            return;
        this.isInterceptionEnabled = true;
        this.patchGlobalFetch();
        this.patchNodeHttp();
    }
    patchGlobalFetch() {
        if (!global.fetch)
            return;
        const originalFetch = global.fetch;
        global.fetch = async (input, init) => {
            const url = typeof input === 'string'
                ? input
                : input instanceof URL
                    ? input.toString()
                    : input.url;
            if (url.includes('127.0.0.1'))
                return originalFetch(input, init);
            const id = Math.random().toString(36).substring(7);
            const method = (init?.method || 'GET').toUpperCase();
            const newInit = { ...init };
            const headers = new Headers(init?.headers || {});
            headers.set(ACTIVITY_ID_HEADER, id);
            newInit.headers = headers;
            let reqBody = '';
            if (init?.body) {
                if (typeof init.body === 'string')
                    reqBody = init.body;
                else if (init.body instanceof URLSearchParams)
                    reqBody = init.body.toString();
            }
            this.requestStartTimes.set(id, Date.now());
            this.safeEmitNetwork({
                id,
                timestamp: Date.now(),
                method,
                url,
                headers: this.stringifyHeaders(newInit.headers),
                body: reqBody,
                pending: true,
            });
            try {
                const response = await originalFetch(input, newInit);
                const clonedRes = response.clone();
                clonedRes
                    .text()
                    .then((text) => {
                    const startTime = this.requestStartTimes.get(id);
                    const durationMs = startTime ? Date.now() - startTime : 0;
                    this.requestStartTimes.delete(id);
                    this.safeEmitNetwork({
                        id,
                        pending: false,
                        response: {
                            status: response.status,
                            headers: this.stringifyHeaders(response.headers),
                            body: text,
                            durationMs,
                        },
                    });
                })
                    .catch((err) => {
                    const message = err instanceof Error ? err.message : String(err);
                    this.safeEmitNetwork({
                        id,
                        pending: false,
                        error: `Failed to read response body: ${message}`,
                    });
                });
                return response;
            }
            catch (err) {
                this.requestStartTimes.delete(id);
                const message = err instanceof Error ? err.message : String(err);
                this.safeEmitNetwork({ id, pending: false, error: message });
                throw err;
            }
        };
    }
    patchNodeHttp() {
        const self = this;
        const originalRequest = http.request;
        const originalHttpsRequest = https.request;
        const wrapRequest = (originalFn, args, protocol) => {
            const options = args[0];
            const url = typeof options === 'string'
                ? options
                : options.href ||
                    `${protocol}//${options.hostname || options.host || 'localhost'}${options.path || '/'}`;
            if (url.includes('127.0.0.1'))
                return originalFn.apply(http, args);
            const headers = typeof options === 'object' && typeof options !== 'function'
                ? options.headers
                : {};
            if (headers && headers[ACTIVITY_ID_HEADER]) {
                delete headers[ACTIVITY_ID_HEADER];
                return originalFn.apply(http, args);
            }
            const id = Math.random().toString(36).substring(7);
            self.requestStartTimes.set(id, Date.now());
            const req = originalFn.apply(http, args);
            const requestChunks = [];
            const oldWrite = req.write;
            const oldEnd = req.end;
            req.write = function (chunk, ...etc) {
                if (chunk) {
                    const encoding = typeof etc[0] === 'string' ? etc[0] : undefined;
                    requestChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding));
                }
                return oldWrite.apply(this, [chunk, ...etc]);
            };
            req.end = function (chunk, ...etc) {
                if (chunk && typeof chunk !== 'function') {
                    const encoding = typeof etc[0] === 'string' ? etc[0] : undefined;
                    requestChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding));
                }
                const body = Buffer.concat(requestChunks).toString('utf8');
                self.safeEmitNetwork({
                    id,
                    timestamp: Date.now(),
                    method: req.method || 'GET',
                    url,
                    headers: self.stringifyHeaders(req.getHeaders()),
                    body,
                    pending: true,
                });
                return oldEnd.apply(this, [chunk, ...etc]);
            };
            req.on('response', (res) => {
                const responseChunks = [];
                res.on('data', (chunk) => responseChunks.push(Buffer.from(chunk)));
                res.on('end', () => {
                    const buffer = Buffer.concat(responseChunks);
                    const encoding = res.headers['content-encoding'];
                    const processBuffer = (finalBuffer) => {
                        const resBody = finalBuffer.toString('utf8');
                        const startTime = self.requestStartTimes.get(id);
                        const durationMs = startTime ? Date.now() - startTime : 0;
                        self.requestStartTimes.delete(id);
                        self.safeEmitNetwork({
                            id,
                            pending: false,
                            response: {
                                status: res.statusCode,
                                headers: self.stringifyHeaders(res.headers),
                                body: resBody,
                                durationMs,
                            },
                        });
                    };
                    if (encoding === 'gzip') {
                        zlib.gunzip(buffer, (err, decompressed) => {
                            processBuffer(err ? buffer : decompressed);
                        });
                    }
                    else if (encoding === 'deflate') {
                        zlib.inflate(buffer, (err, decompressed) => {
                            processBuffer(err ? buffer : decompressed);
                        });
                    }
                    else {
                        processBuffer(buffer);
                    }
                });
            });
            req.on('error', (err) => {
                self.requestStartTimes.delete(id);
                const message = err instanceof Error ? err.message : String(err);
                self.safeEmitNetwork({ id, pending: false, error: message });
            });
            return req;
        };
        http.request = (...args) => wrapRequest(originalRequest, args, 'http:');
        https.request = (...args) => wrapRequest(originalHttpsRequest, args, 'https:');
    }
    logConsole(payload) {
        this.emit('console', payload);
    }
}
/**
 * Registers the activity logger.
 * Captures network and console logs to a session-specific JSONL file.
 *
 * The log file location can be overridden via the GEMINI_CLI_ACTIVITY_LOG_FILE
 * environment variable. If not set, defaults to logs/session-{sessionId}.jsonl
 * in the project's temp directory.
 *
 * @param config The CLI configuration
 */
export function registerActivityLogger(config) {
    if (config.storage) {
        const capture = ActivityLogger.getInstance();
        capture.enable();
        const logsDir = config.storage.getProjectTempLogsDir();
        if (!fs.existsSync(logsDir)) {
            fs.mkdirSync(logsDir, { recursive: true });
        }
        const logFile = process.env['GEMINI_CLI_ACTIVITY_LOG_FILE'] ||
            path.join(logsDir, `session-${config.getSessionId()}.jsonl`);
        const writeToLog = (type, payload) => {
            try {
                const entry = JSON.stringify({
                    type,
                    payload,
                    timestamp: Date.now(),
                }) + '\n';
                // Use asynchronous fire-and-forget to avoid blocking the event loop
                fs.promises.appendFile(logFile, entry).catch((err) => {
                    debugLogger.error('Failed to write to activity log:', err);
                });
            }
            catch (err) {
                debugLogger.error('Failed to prepare activity log entry:', err);
            }
        };
        capture.on('console', (payload) => writeToLog('console', payload));
        capture.on('network', (payload) => writeToLog('network', payload));
        // Bridge CoreEvents to local capture
        coreEvents.on(CoreEvent.ConsoleLog, (payload) => {
            capture.logConsole(payload);
        });
    }
}
//# sourceMappingURL=activityLogger.js.map