"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.skipDevConsoleObserver = skipDevConsoleObserver;
exports.initDevConsole = initDevConsole;
exports.startDevConsoleSrpcServer = startDevConsoleSrpcServer;
exports.serializeError = serializeError;
function __assignType(fn, args) {
    fn.__type = args;
    return fn;
}
const services_1 = require("../services");
const uuid_1 = require("../helpers/utils/uuid");
const devconsole_srpc_1 = require("./devconsole.srpc");
const devconsole_controller_1 = require("./devconsole.controller");
const devconsole_middleware_1 = require("./devconsole.middleware");
const devconsole_store_1 = require("./devconsole.store");
const _skipObserverSet = (WeakSet.Ω = [['%']], new WeakSet());
function skipDevConsoleObserver(server) {
    _skipObserverSet.add(server);
}
skipDevConsoleObserver.__type = ['server', 'skipDevConsoleObserver', 'P%2!"/"'];
let _patchesApplied = false;
function initDevConsole(app) {
    const store = devconsole_store_1.DevConsoleStore.init();
    app.appModule.addProvider(devconsole_middleware_1.DevConsoleLocalhostMiddleware);
    app.appModule.addController(devconsole_controller_1.DevConsoleController);
    if (!_patchesApplied) {
        _patchesApplied = true;
        patchHttpKernel(store);
        patchHttpWorkflow();
        patchWorkerRecorder(store);
        patchSrpcClient(store);
        patchSrpcServer(store);
        patchMutex(store);
        patchDatabase(store);
    }
}
initDevConsole.__type = ['App', 'app', 'initDevConsole', 'P"w!2""/#'];
function startDevConsoleSrpcServer() {
    const store = devconsole_store_1.DevConsoleStore.get();
    if (!store)
        return;
    try {
        // eslint-disable-next-line @typescript-eslint/no-require-imports
        const { DevConsoleSrpcServer } = require('./devconsole.ws');
        const srpcServer = new DevConsoleSrpcServer((0, services_1.createLogger)('DevConsole'));
        store.onEvent = __assignType((type, data) => srpcServer.broadcast(type, data), ['type', 'data', '', 'P"2!"2""/#']);
    }
    catch (err) {
        console.warn('[DevConsole] Failed to start SRPC server', err);
    }
}
startDevConsoleSrpcServer.__type = ['startDevConsoleSrpcServer', 'P"/!'];
////////////////////////////////////////
// HTTP Kernel
function patchHttpKernel(store) {
    // eslint-disable-next-line @typescript-eslint/no-require-imports
    const { CustomHttpKernel } = require('../http/kernel');
    const BODY_LIMIT = 32 * 1024;
    const origSkip = CustomHttpKernel.prototype.shouldSkipRequestLogging;
    CustomHttpKernel.prototype.shouldSkipRequestLogging = __assignType(function (request) {
        return origSkip.call(this, request) || request.url?.startsWith('/_devconsole');
    }, ['HttpRequest', 'request', '', 'P"w!2""/#']);
    const origHandleRequest = CustomHttpKernel.prototype.handleRequest;
    CustomHttpKernel.prototype.handleRequest = __assignType(async function (req, res) {
        if (req.url?.startsWith('/_devconsole')) {
            return origHandleRequest.call(this, req, res);
        }
        const responseChunks = [];
        let responseTotalBytes = 0;
        // Capture all response headers at the writeHead() funnel point.
        // Headers set via setHeader() are in the internal map; headers passed
        // directly to writeHead() are not.  Merge both here.
        const capturedHeaders = {};
        const origWriteHead = res.writeHead.bind(res);
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        res.writeHead = __assignType(function (statusCode, ...args) {
            // Grab setHeader() headers first, then overlay writeHead() argument headers
            for (const [key, val] of Object.entries(res.getHeaders())) {
                if (val != null)
                    capturedHeaders[key] = String(val);
            }
            // writeHead(status, headers) or writeHead(status, statusMessage, headers)
            const headers = typeof args[0] === 'object' ? args[0] : args[1];
            if (headers) {
                for (const key of Object.keys(headers)) {
                    capturedHeaders[key.toLowerCase()] = headers[key];
                }
            }
            return origWriteHead(statusCode, ...args);
        }, ['statusCode', 'args', '', 'P\'2!"@2""/#']);
        const origWrite = res.write.bind(res);
        const origEnd = res.end.bind(res);
        res.write = __assignType(function (chunk, ...args) {
            if (chunk) {
                const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
                responseTotalBytes += buf.length;
                if (responseTotalBytes <= BODY_LIMIT) {
                    responseChunks.push(buf);
                }
            }
            return origWrite(chunk, ...args);
        }, ['chunk', 'args', '', 'P"2!"@2""/#']);
        res.end = __assignType(function (chunk, ...args) {
            if (chunk) {
                const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
                responseTotalBytes += buf.length;
                if (responseTotalBytes <= BODY_LIMIT) {
                    responseChunks.push(buf);
                }
            }
            return origEnd(chunk, ...args);
        }, ['chunk', 'args', '', 'P"2!"@2""/#']);
        try {
            await origHandleRequest.call(this, req, res);
        }
        finally {
            const contentType = capturedHeaders['content-type'] ?? '';
            const isText = contentType.includes('json') || contentType.includes('text') || contentType.includes('xml');
            let responseBody = null;
            if (responseChunks.length) {
                if (isText) {
                    responseBody = Buffer.concat(responseChunks).toString('utf8');
                    if (responseBody.length > BODY_LIMIT) {
                        responseBody = responseBody.slice(0, BODY_LIMIT) + '...(truncated)';
                    }
                }
                else {
                    responseBody = `<binary ${responseTotalBytes} bytes>`;
                }
            }
            let requestBody = null;
            const reqContentType = String(req.headers['content-type'] ?? '');
            const reqIsText = reqContentType.includes('json') ||
                reqContentType.includes('text') ||
                reqContentType.includes('xml') ||
                reqContentType.includes('form');
            if (req.body) {
                if (reqIsText) {
                    requestBody = req.body.toString('utf8').slice(0, BODY_LIMIT);
                }
                else {
                    requestBody = `<binary ${req.body.length} bytes>`;
                }
            }
            const controllerError = req.store['$ControllerError'];
            const errorInfo = controllerError ? serializeError(controllerError) : undefined;
            store.addHttpEntry({
                id: (0, uuid_1.uuid7)(),
                timestamp: req.store['$RequestTime'] ?? Date.now(),
                method: req.method ?? 'UNKNOWN',
                url: req.url ?? '',
                remoteAddress: req.socket?.remoteAddress ?? '',
                requestHeaders: req.headers,
                requestBody,
                statusCode: res.statusCode,
                responseHeaders: capturedHeaders,
                responseBody,
                durationMs: Date.now() - (req.store['$RequestTime'] ?? Date.now()),
                error: errorInfo
            });
        }
    }, ['HttpRequest', 'req', 'HttpResponse', 'res', '', 'P"w!2""w#2$"/%']);
}
patchHttpKernel.__type = [() => devconsole_store_1.DevConsoleStore, 'store', 'patchHttpKernel', 'PP7!2""/#'];
////////////////////////////////////////
// HTTP Workflow (error capture for request inspector)
function patchHttpWorkflow() {
    // eslint-disable-next-line @typescript-eslint/no-require-imports
    const { HttpWorkflowListener } = require('../http/workflow');
    const origOnParametersFailed = HttpWorkflowListener.prototype.onParametersFailed;
    HttpWorkflowListener.prototype.onParametersFailed = __assignType(function (event) {
        event.request.store['$ControllerError'] = event.error;
        return origOnParametersFailed.call(this, event);
    }, ['event', '', 'P"2!"/"']);
    const origOnControllerError = HttpWorkflowListener.prototype.onControllerError;
    HttpWorkflowListener.prototype.onControllerError = __assignType(function (event) {
        event.request.store['$ControllerError'] = event.error;
        return origOnControllerError.call(this, event);
    }, ['event', '', 'P"2!"/"']);
}
patchHttpWorkflow.__type = ['patchHttpWorkflow', 'P"/!'];
////////////////////////////////////////
// Worker Recorder
function patchWorkerRecorder(store) {
    let WorkerRecorderService;
    try {
        // eslint-disable-next-line @typescript-eslint/no-require-imports
        WorkerRecorderService = require('../services/worker/recorder').WorkerRecorderService;
    }
    catch {
        return;
    }
    const origStart = WorkerRecorderService.prototype.start;
    WorkerRecorderService.prototype.start = __assignType(async function () {
        await origStart.call(this);
        const observer = this.observer;
        const queueName = this.queueName;
        if (observer) {
            observer.on('added', __assignType((args) => {
                store.onEvent?.('worker:added', { queue: queueName, queueId: args.jobId, name: args.name, status: 'waiting' });
            }, ['args', '', 'P"2!"/"']));
            observer.on('active', __assignType((args) => {
                store.onEvent?.('worker:active', { queue: queueName, queueId: args.jobId, status: 'active' });
            }, ['args', '', 'P"2!"/"']));
            observer.on('delayed', __assignType((args) => {
                store.onEvent?.('worker:delayed', { queue: queueName, queueId: args.jobId, status: 'delayed' });
            }, ['args', '', 'P"2!"/"']));
        }
    }, ['this', '', 'P"2!"/"']);
    const origLogJob = WorkerRecorderService.prototype.logJob;
    WorkerRecorderService.prototype.logJob = __assignType(async function (job, status, result) {
        await origLogJob.call(this, job, status, result);
        const traceparent = job.opts?.traceparent;
        const traceId = traceparent ? traceparent.split('-')[1] : null;
        store.onEvent?.('worker:job', {
            id: `${this.queueName}:${job.id}`,
            queue: this.queueName,
            queueId: job.id,
            name: job.name,
            data: job.data,
            status,
            result,
            attempt: job.attemptsMade,
            traceId,
            createdAt: job.timestamp,
            shouldExecuteAt: job.timestamp + (job.opts?.delay ?? 0),
            executedAt: job.processedOn ?? null,
            completedAt: job.finishedOn ?? null
        });
    }, ['this', 'job', "completed", "failed", 'status', 'result', '', 'P"2!"2"P.#.$J2%"2&"/\'']);
}
patchWorkerRecorder.__type = [() => devconsole_store_1.DevConsoleStore, 'store', 'patchWorkerRecorder', 'PP7!2""/#'];
////////////////////////////////////////
// SRPC Client
function patchSrpcClient(store) {
    // eslint-disable-next-line @typescript-eslint/no-require-imports
    const { SrpcClient } = require('../srpc/SrpcClient');
    const observer = (0, devconsole_srpc_1.createDevConsoleSrpcObserver)(store);
    const connectedAtMap = (WeakMap.Ω = [['%'], ['\'']], new WeakMap());
    function buildObserverStream(client) {
        return {
            id: client.streamId,
            clientId: client.clientId,
            clientStreamId: client.streamId,
            appVersion: '0.0.0',
            address: client.uri,
            configureTs: 0,
            connectedAt: connectedAtMap.get(client) ?? Date.now(),
            lastPingAt: client.lastPongMs ?? Date.now(),
            meta: client.clientMeta ?? {}
        };
    }
    buildObserverStream.__type = ['client', 'buildObserverStream', 'P"2!"/"'];
    const origHandshake = SrpcClient.prototype.handleInitialHandshake;
    SrpcClient.prototype.handleInitialHandshake = __assignType(function (ws, data, clearConnectTimeout) {
        origHandshake.call(this, ws, data, clearConnectTimeout);
        if (this.isConnected) {
            connectedAtMap.set(this, Date.now());
            observer.onConnectionEstablished?.(buildObserverStream(this));
        }
    }, ['this', 'ws', 'data', 'clearConnectTimeout', '', 'P"2!"2""2#"2$"/%']);
    const origDisconnect = SrpcClient.prototype.handleDisconnect;
    SrpcClient.prototype.handleDisconnect = __assignType(function () {
        const wasConnected = this.isConnected;
        const stream = wasConnected ? buildObserverStream(this) : null;
        origDisconnect.call(this);
        if (stream) {
            observer.onConnectionClosed?.(stream, 'disconnect');
        }
    }, ['this', '', 'P"2!"/"']);
    const origHandleMessage = SrpcClient.prototype.handleMessage;
    SrpcClient.prototype.handleMessage = __assignType(async function (data) {
        const message = this.decodeMessage(data);
        if (message && !message.pingPong && !message.byteStreamOperation) {
            const msgType = (0, devconsole_srpc_1.identifyMessageType)(message);
            observer.onInboundMessage?.(buildObserverStream(this), msgType, message);
        }
        await origHandleMessage.call(this, data);
    }, ['this', 'data', '', 'P"2!!2""/#']);
    const origWriteMessage = SrpcClient.prototype.writeMessage;
    SrpcClient.prototype.writeMessage = __assignType(function (message) {
        if (!message.pingPong && !message.byteStreamOperation) {
            const msgType = (0, devconsole_srpc_1.identifyMessageType)(message);
            observer.onOutboundMessage?.(buildObserverStream(this), msgType, message);
        }
        return origWriteMessage.call(this, message);
    }, ['this', 'message', '', 'P"2!"2""/#']);
}
patchSrpcClient.__type = [() => devconsole_store_1.DevConsoleStore, 'store', 'patchSrpcClient', 'PP7!2""/#'];
////////////////////////////////////////
// SRPC Server
function patchSrpcServer(store) {
    // eslint-disable-next-line @typescript-eslint/no-require-imports
    const { SrpcServer } = require('../srpc/SrpcServer');
    const observer = (0, devconsole_srpc_1.createDevConsoleSrpcObserver)(store);
    const origEstablished = SrpcServer.prototype.handleStreamEstablished;
    SrpcServer.prototype.handleStreamEstablished = __assignType(function (stream) {
        origEstablished.call(this, stream);
        if (!_skipObserverSet.has(this)) {
            observer.onConnectionEstablished?.(stream);
        }
    }, ['this', 'stream', '', 'P"2!"2""/#']);
    const origCleanup = SrpcServer.prototype.cleanupStream;
    SrpcServer.prototype.cleanupStream = __assignType(function (stream, forceCause) {
        const shouldObserve = !_skipObserverSet.has(this) && stream.lastPingAt >= 0;
        origCleanup.call(this, stream, forceCause);
        if (shouldObserve) {
            observer.onConnectionClosed?.(stream, forceCause ?? 'disconnect');
        }
    }, ['this', 'stream', 'forceCause', '', 'P"2!"2"&2#8"/$']);
    const origDataReceived = SrpcServer.prototype.handleStreamDataReceived;
    SrpcServer.prototype.handleStreamDataReceived = __assignType(function (stream, data) {
        if (!_skipObserverSet.has(this) && !data.pingPong && !data.byteStreamOperation) {
            const msgType = (0, devconsole_srpc_1.identifyMessageType)(data);
            observer.onInboundMessage?.(stream, msgType, data);
        }
        origDataReceived.call(this, stream, data);
    }, ['this', 'stream', 'data', '', 'P"2!"2""2#"/$']);
    const origWriteToStream = SrpcServer.prototype.writeToStream;
    SrpcServer.prototype.writeToStream = __assignType(function (stream, data) {
        if (!_skipObserverSet.has(this) && !data.pingPong && !data.byteStreamOperation) {
            const msgType = (0, devconsole_srpc_1.identifyMessageType)(data);
            observer.onOutboundMessage?.(stream, msgType, data);
        }
        origWriteToStream.call(this, stream, data);
    }, ['this', 'stream', 'data', '', 'P"2!"2""2#"/$']);
}
patchSrpcServer.__type = [() => devconsole_store_1.DevConsoleStore, 'store', 'patchSrpcServer', 'PP7!2""/#'];
////////////////////////////////////////
// Mutex
function patchMutex(store) {
    // eslint-disable-next-line @typescript-eslint/no-require-imports
    const mutexModule = require('../helpers/redis/mutex');
    const { flattenMutexKey, MutexAcquisitionError } = mutexModule;
    const origWithMutex = mutexModule.withMutex;
    mutexModule.withMutex = __assignType(function (options) {
        const key = flattenMutexKey(options.key);
        const id = (0, uuid_1.uuid7)();
        const startedAt = Date.now();
        const entry = {
            id,
            key,
            status: 'pending',
            startedAt
        };
        store.addMutexPending(entry);
        const wrappedOptions = {
            ...options,
            fn: __assignType(async (didWait) => {
                store.updateMutexAcquired(id, didWait);
                try {
                    const result = await options.fn(didWait);
                    store.updateMutexReleased(id);
                    return result;
                }
                catch (err) {
                    store.updateMutexError(id, err);
                    throw err;
                }
            }, ['didWait', '', 'P)2!"/"'])
        };
        return origWithMutex(wrappedOptions).catch(__assignType((err) => {
            if (err instanceof MutexAcquisitionError) {
                // Update the pending entry to failed
                const existing = store.activeMutexes.get(id);
                if (existing) {
                    existing.status = 'failed';
                    existing.error = err.message;
                    store.activeMutexes.delete(id);
                    store.onEvent?.('mutex:failed', existing);
                }
                else {
                    store.addMutexFailed({
                        id,
                        key,
                        status: 'failed',
                        startedAt,
                        error: err.message
                    });
                }
            }
            throw err;
        }, ['err', '', 'P"2!"/"']));
    }, ['options', '', 'P"2!"`/"']);
}
patchMutex.__type = [() => devconsole_store_1.DevConsoleStore, 'store', 'patchMutex', 'PP7!2""/#'];
////////////////////////////////////////
// Database
function patchDatabase(store) {
    function startEntry(sql, params) {
        const entry = {
            id: (0, uuid_1.uuid7)(),
            timestamp: Date.now(),
            sql,
            params,
            status: 'running'
        };
        store.addDatabaseQuery(entry);
        return entry;
    }
    startEntry.__type = ['sql', 'params', 'DevConsoleDatabaseQueryEntry', 'startEntry', 'P&2!"F2""w#/$'];
    function completeEntry(entry, error) {
        entry.durationMs = Date.now() - entry.timestamp;
        if (error) {
            entry.status = 'error';
            entry.error = error;
        }
        else {
            entry.status = 'ok';
        }
        store.completeDatabaseQuery(entry);
    }
    completeEntry.__type = ['DevConsoleDatabaseQueryEntry', 'entry', 'error', 'completeEntry', 'P"w!2"&2#8"/$'];
    // Patch writes: MySQLConnection.run() and PostgresConnection.run()
    try {
        // eslint-disable-next-line @typescript-eslint/no-require-imports
        const { MySQLConnection } = require('@deepkit/mysql');
        const origMysqlRun = MySQLConnection.prototype.run;
        MySQLConnection.prototype.run = __assignType(async function (sql, params = []) {
            const entry = startEntry(sql, params);
            try {
                const result = await origMysqlRun.call(this, sql, params);
                completeEntry(entry);
                return result;
            }
            catch (err) {
                completeEntry(entry, err.message || String(err));
                throw err;
            }
        }, ['sql', 'params', () => [], '', 'P&2!"F2">#"/$']);
    }
    catch {
        // @deepkit/mysql not available
    }
    try {
        // eslint-disable-next-line @typescript-eslint/no-require-imports
        const { PostgresConnection } = require('@deepkit/postgres');
        const origPgRun = PostgresConnection.prototype.run;
        PostgresConnection.prototype.run = __assignType(async function (sql, params = []) {
            const entry = startEntry(sql, params);
            try {
                const result = await origPgRun.call(this, sql, params);
                completeEntry(entry);
                return result;
            }
            catch (err) {
                completeEntry(entry, err.message || String(err));
                throw err;
            }
        }, ['sql', 'params', () => [], '', 'P&2!"F2">#"/$']);
    }
    catch {
        // @deepkit/postgres not available
    }
    // Patch reads: SQLConnection.execAndReturnAll() and execAndReturnSingle()
    try {
        // eslint-disable-next-line @typescript-eslint/no-require-imports
        const { SQLConnection } = require('@deepkit/sql');
        const origExecAll = SQLConnection.prototype.execAndReturnAll;
        SQLConnection.prototype.execAndReturnAll = __assignType(async function (sql, params) {
            const entry = startEntry(sql, params ?? []);
            try {
                const result = await origExecAll.call(this, sql, params);
                completeEntry(entry);
                return result;
            }
            catch (err) {
                completeEntry(entry, err.message || String(err));
                throw err;
            }
        }, ['sql', 'params', '', 'P&2!"F2"8"/#']);
        const origExecSingle = SQLConnection.prototype.execAndReturnSingle;
        SQLConnection.prototype.execAndReturnSingle = __assignType(async function (sql, params) {
            const entry = startEntry(sql, params ?? []);
            try {
                const result = await origExecSingle.call(this, sql, params);
                completeEntry(entry);
                return result;
            }
            catch (err) {
                completeEntry(entry, err.message || String(err));
                throw err;
            }
        }, ['sql', 'params', '', 'P&2!"F2"8"/#']);
    }
    catch {
        // @deepkit/sql not available
    }
}
patchDatabase.__type = [() => devconsole_store_1.DevConsoleStore, 'store', 'patchDatabase', 'PP7!2""/#'];
////////////////////////////////////////
// Error serialization helper
const SKIP_KEYS = new Set(['name', 'message', 'stack', 'cause']);
function serializeError(err) {
    if (!(err instanceof Error)) {
        return { name: 'Error', message: String(err) };
    }
    const info = {
        name: err.name,
        message: err.message,
        stack: err.stack
    };
    for (const key of Object.keys(err)) {
        if (SKIP_KEYS.has(key))
            continue;
        info[key] = err[key];
    }
    const cause = err.cause;
    if (cause instanceof Error) {
        info.cause = serializeError(cause);
    }
    else if (cause !== undefined) {
        info.cause = { name: 'Error', message: String(cause) };
    }
    return info;
}
serializeError.__type = ['err', 'DevConsoleErrorInfo', 'serializeError', 'P#2!"w"/#'];
//# sourceMappingURL=patches.js.map