"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DevConsoleSrpcServer = void 0;
exports.collectProperties = collectProperties;
const tslib_1 = require("tslib");
const _a = tslib_1.__importStar(require("@zyno-io/ts-server-foundation/type-metadata-runtime"));
const node_util_1 = require("node:util");
const database_1 = require("../database");
const health_1 = require("../health");
const helpers_1 = require("../helpers");
const worker_1 = require("../services/worker");
const srpc_1 = require("../srpc");
const devconsole_1 = require("./generated/devconsole");
const security_1 = require("./security");
const __tsf_metadata_type = _a.createCompactMetadataRegistryV1("[1,[{\"kind\":18,\"typeName\":\"Record\",\"utilityType\":\"Record\",\"typeArguments\":[{\"kind\":6},{\"kind\":1}],\"index\":{\"kind\":1},\"types\":[]},{\"kind\":3},{\"kind\":6},{\"kind\":2}]]", []);
const SECRET_MASK_PATTERNS = ['SECRET', 'PASSWORD', 'DSN', 'TOKEN', 'KEY'];
let _DevConsoleSrpcServerMetadata_1;
class DevConsoleSrpcServer {
    app;
    store;
    server;
    replContext;
    constructor(app, store, logger, httpServer) {
        this.app = app;
        this.store = store;
        this.replContext = createReplContext(app);
        this.server = new srpc_1.SrpcServer({
            logger,
            clientMessage: devconsole_1.DevConsoleClientMessage,
            serverMessage: devconsole_1.DevConsoleServerMessage,
            wsPath: '/_devconsole/ws',
            httpServer,
            logLevel: false
        });
        this.server.setClientAuthorizer((_meta, req) => ((0, security_1.isLocalhostIncomingMessage)(req) ? { devconsole: true } : false));
        this.registerHandlers();
    }
    close() {
        this.server.close();
    }
    registerHandlers() {
        this.server.registerMessageHandler('uReplEval', async (_stream, data) => this.handleReplEval(data.code));
        this.server.registerMessageHandler('uReplComplete', (_stream, data) => this.handleReplComplete(data.code, data.cursorPos));
        this.server.registerMessageHandler('uGetOverview', () => ({
            name: (0, helpers_1.getPackageName)() ?? '',
            version: (0, helpers_1.getPackageVersion)() ?? '',
            uptime: Date.now() - this.store.startedAt,
            env: this.app.config.APP_ENV,
            httpEntries: this.store.httpEntries.length,
            srpcMessages: this.store.srpcMessages.length,
            srpcActiveConnections: this.store.srpcConnections.size,
            srpcDisconnected: this.store.srpcDisconnected.length
        }));
        this.server.registerMessageHandler('uGetProcess', () => {
            const mem = process.memoryUsage();
            const cpu = process.cpuUsage();
            return {
                pid: process.pid,
                rss: mem.rss,
                heapTotal: mem.heapTotal,
                heapUsed: mem.heapUsed,
                external: mem.external,
                arrayBuffers: mem.arrayBuffers,
                cpuUser: cpu.user,
                cpuSystem: cpu.system,
                uptime: process.uptime(),
                nodeVersion: process.version,
                platform: process.platform,
                arch: process.arch
            };
        });
        this.server.registerMessageHandler('uGetEnv', () => ({
            jsonData: (0, helpers_1.safeJsonStringify)(maskSecrets(this.app.config))
        }));
        this.server.registerMessageHandler('uGetRequests', () => ({
            jsonData: (0, helpers_1.safeJsonStringify)(this.store.httpEntries.toArray().reverse())
        }));
        this.server.registerMessageHandler('uGetRoutes', () => ({
            routes: this.app.router
                .listRoutes()
                .filter(route => !route.path.startsWith('/_devconsole'))
                .map(route => ({
                methods: [route.method],
                path: route.path,
                controller: route.controllerClass.name,
                methodName: route.methodName
            }))
        }));
        this.server.registerMessageHandler('uGetSrpc', () => ({
            jsonData: (0, helpers_1.safeJsonStringify)({
                active: [...this.store.srpcConnections.values()],
                recentDisconnections: this.store.srpcDisconnected.toArray().reverse()
            })
        }));
        this.server.registerMessageHandler('uGetSrpcMessages', (_stream, data) => {
            let messages = this.store.srpcMessages.toArray();
            if (data.streamId)
                messages = messages.filter(message => message.streamId === data.streamId);
            return { jsonData: (0, helpers_1.safeJsonStringify)(messages.reverse()) };
        });
        this.server.registerMessageHandler('uGetWorkers', () => this.getWorkers());
        this.server.registerMessageHandler('uGetWorkersJobs', () => this.getWorkerJobs());
        this.server.registerMessageHandler('uGetDatabaseEntities', () => this.getDatabaseEntities());
        this.server.registerMessageHandler('uDatabaseQuery', (_stream, data) => this.runDatabaseQuery(data.sql));
        this.server.registerMessageHandler('uGetHealthChecks', async () => ({
            jsonData: (0, helpers_1.safeJsonStringify)((await tryGet(this.app, health_1.HealthcheckService)?.checkIndividual()) ?? [])
        }));
        this.server.registerMessageHandler('uGetDatabaseQueries', () => ({
            jsonData: (0, helpers_1.safeJsonStringify)(this.store.dbQueries.toArray().reverse())
        }));
        this.server.registerMessageHandler('uClearDatabaseQueries', () => {
            this.store.clearDatabaseQueries();
            this.broadcast('db:cleared', {});
            return {};
        });
        this.server.registerMessageHandler('uClearRequests', () => {
            this.store.clearHttpEntries();
            this.broadcast('http:cleared', {});
            return {};
        });
        this.server.registerMessageHandler('uClearSrpcMessages', () => {
            this.store.clearSrpcMessages();
            this.broadcast('srpc:cleared', {});
            return {};
        });
        this.server.registerMessageHandler('uGetMutexes', () => ({
            jsonData: (0, helpers_1.safeJsonStringify)({
                active: [...this.store.activeMutexes.values()],
                history: this.store.mutexEntries.toArray().reverse()
            })
        }));
    }
    async getWorkers() {
        const registry = tryGet(this.app, worker_1.WorkerQueueRegistry);
        if (!registry)
            return { jsonData: (0, helpers_1.safeJsonStringify)({}) };
        if (registry.usesBullMq()) {
            const result = {};
            for (const { name, queue } of registry.getBullQueues()) {
                try {
                    result[name] = await queue.getJobCounts();
                }
                catch {
                    result[name] = { error: 'Failed to fetch job counts' };
                }
            }
            return { jsonData: (0, helpers_1.safeJsonStringify)(result) };
        }
        const result = {};
        for (const job of registry.getAllQueuedJobs()) {
            const counts = (result[job.queue] ??= {});
            counts[job.status] = (counts[job.status] ?? 0) + 1;
        }
        return { jsonData: (0, helpers_1.safeJsonStringify)(result) };
    }
    async getWorkerJobs() {
        const registry = tryGet(this.app, worker_1.WorkerQueueRegistry);
        const recorder = tryGet(this.app, worker_1.WorkerRecorderService);
        const live = registry ? await this.getLiveWorkerJobs(registry) : [];
        const history = recorder ? recorder.getRecords().map(recordToWorkerJob).reverse().slice(0, 200) : [];
        return { jsonData: (0, helpers_1.safeJsonStringify)({ live, history }) };
    }
    async getLiveWorkerJobs(registry) {
        if (!registry.usesBullMq()) {
            return registry.getAllQueuedJobs().map(job => ({
                id: `${job.queue}:${job.id}`,
                queue: job.queue,
                queueId: job.id,
                name: job.name,
                data: job.data,
                status: job.shouldExecuteAt.getTime() > Date.now() ? 'delayed' : 'waiting',
                attempt: job.attemptsMade,
                createdAt: job.createdAt.getTime(),
                shouldExecuteAt: job.shouldExecuteAt.getTime(),
                executedAt: null
            }));
        }
        const live = [];
        for (const { name: queueName, queue } of registry.getBullQueues()) {
            try {
                const [active, waiting, delayed] = await Promise.all([queue.getActive(), queue.getWaiting(), queue.getDelayed()]);
                for (const job of active)
                    live.push(bullJobToDevConsoleJob(queueName, job, 'active'));
                for (const job of waiting)
                    live.push(bullJobToDevConsoleJob(queueName, job, 'waiting'));
                for (const job of delayed)
                    live.push(bullJobToDevConsoleJob(queueName, job, 'delayed'));
            }
            catch {
                // A queue can disappear or Redis can be unavailable while the panel is open.
            }
        }
        return live;
    }
    getDatabaseEntities() {
        const db = tryGet(this.app, database_1.BaseDatabase);
        if (!db)
            return { entities: [] };
        const entities = db.entityRegistry
            .map(entity => {
            const metadata = (0, database_1.getEntityMetadata)(entity);
            return {
                name: metadata.classType.name,
                table: metadata.tableName,
                columns: metadata.columns.map(column => column.propertyName),
                quotedTable: (0, database_1.renderSql)((0, database_1.sql) `${database_1.sql.identifier(metadata.tableName)}`, db.driver.dialect).sql
            };
        })
            .sort((a, b) => a.name.localeCompare(b.name));
        return { entities };
    }
    async runDatabaseQuery(text) {
        const db = tryGet(this.app, database_1.BaseDatabase);
        if (!db)
            return { columns: [], rows: [], rowCount: 0, error: 'No database is configured' };
        const query = text.trim();
        if (!query)
            return { columns: [], rows: [], rowCount: 0, error: 'Empty query' };
        try {
            if (/^\s*(select|show|describe|explain|with)\b/i.test(query)) {
                const rawRows = await db.rawFindUnsafe(query, undefined, undefined, (__tsf_metadata_type(0)));
                const columns = rawRows[0] ? Object.keys(rawRows[0]) : [];
                const rows = rawRows.map(row => ({
                    values: columns.map(column => stringifyCell(row[column]))
                }));
                return { columns, rows, rowCount: rawRows.length };
            }
            const result = await db.rawExecuteUnsafe(query);
            return {
                columns: [],
                rows: [],
                rowCount: result.rowCount ?? result.affectedRows,
                affectedRows: result.affectedRows
            };
        }
        catch (error) {
            return {
                columns: [],
                rows: [],
                rowCount: 0,
                error: error instanceof Error ? error.message : String(error)
            };
        }
    }
    async handleReplEval(code) {
        const logs = [];
        const capture = (...args) => logs.push(args.map(item => (typeof item === 'string' ? item : (0, node_util_1.inspect)(item, { depth: 4, colors: false }))).join(' '));
        const localConsole = {
            log: capture,
            warn: capture,
            error: capture,
            info: capture,
            debug: capture
        };
        try {
            const fn = new Function('context', 'console', `with (context) { return eval(${(0, helpers_1.safeJsonStringify)(code)}) }`);
            let result = fn(this.replContext, localConsole);
            if (result && typeof result === 'object' && typeof result.then === 'function')
                result = await result;
            const resultText = result === undefined ? '' : (0, node_util_1.inspect)(result, { depth: 4, colors: false });
            return { output: [...logs, resultText].filter(Boolean).join('\n') };
        }
        catch (error) {
            return {
                output: logs.join('\n'),
                error: error instanceof Error ? (error.stack ?? error.message) : String(error)
            };
        }
    }
    handleReplComplete(code, cursorPos) {
        const beforeCursor = code.slice(0, cursorPos);
        const match = beforeCursor.match(/((?:\$|r|resolve|app|config|db|container|[a-zA-Z_]\w*)(?:\.[a-zA-Z_]\w*)*(?:\.\w*)?)$/);
        if (!match)
            return { items: [], replaceStart: cursorPos, replaceEnd: cursorPos };
        const fullExpression = match[1];
        const parts = fullExpression.split('.');
        const prefix = parts.pop() ?? '';
        const replaceStart = cursorPos - prefix.length;
        if (parts.length === 0) {
            return {
                items: completeTopLevel(this.replContext, prefix),
                replaceStart,
                replaceEnd: cursorPos
            };
        }
        const target = this.resolveReplExpression(parts.join('.'));
        return {
            items: target == null ? [] : collectProperties(target, prefix).slice(0, 50),
            replaceStart,
            replaceEnd: cursorPos
        };
    }
    resolveReplExpression(expression) {
        try {
            const fn = new Function('context', `with (context) { return ${expression} }`);
            return fn(this.replContext);
        }
        catch {
            return undefined;
        }
    }
    broadcast(type, data) {
        const jsonData = (0, helpers_1.safeJsonStringify)(data);
        for (const stream of this.server.streamsByClientId.values()) {
            this.server.invoke(stream, 'dEvent', { type, jsonData }).catch(() => { });
        }
    }
    static __tsfType = _DevConsoleSrpcServerMetadata_1 = _a.decodeCompactMetadataV1("[1,{\"kind\":16,\"name\":\"DevConsoleSrpcServer\",\"typeName\":\"DevConsoleSrpcServer\",\"classType\":{\"$tsf\":0},\"properties\":[],\"methods\":[{\"name\":\"close\",\"parameters\":[],\"returnType\":{\"$tsfType\":1}},{\"name\":\"broadcast\",\"parameters\":[{\"name\":\"type\",\"type\":{\"$tsfType\":2},\"optional\":false,\"default\":false},{\"name\":\"data\",\"type\":{\"$tsfType\":3},\"optional\":false,\"default\":false}],\"returnType\":{\"$tsfType\":1}}],\"hasConstructor\":true,\"constructorParameters\":[{\"name\":\"app\",\"type\":{\"kind\":16,\"typeName\":\"App\",\"classType\":{\"$tsfImport\":[1,\"../app/index\",\"App\"]},\"typeArguments\":[{\"kind\":1}]},\"optional\":false,\"default\":false},{\"name\":\"store\",\"type\":{\"kind\":16,\"typeName\":\"DevConsoleStore\",\"classType\":{\"$tsfImport\":[1,\"./store\",\"DevConsoleStore\"]}},\"optional\":false,\"default\":false},{\"name\":\"logger\",\"type\":{\"kind\":16,\"typeName\":\"ScopedLogger\",\"classType\":{\"$tsfImport\":[1,\"../services/index\",\"ScopedLogger\"]}},\"optional\":false,\"default\":false},{\"name\":\"httpServer\",\"type\":{\"$tsfAlias\":[1,\"node:http\",\"Server\",\"Server\"]},\"optional\":false,\"default\":false}]}]", [() => DevConsoleSrpcServer, require], __tsf_metadata_type);
}
exports.DevConsoleSrpcServer = DevConsoleSrpcServer;
_DevConsoleSrpcServerMetadata_1.classType = DevConsoleSrpcServer;
DevConsoleSrpcServer.__tsfType = _DevConsoleSrpcServerMetadata_1;
function maskSecrets(config) {
    const masked = {};
    for (const [key, value] of Object.entries(config)) {
        const upper = key.toUpperCase();
        masked[key] = SECRET_MASK_PATTERNS.some(pattern => upper.includes(pattern)) ? '****' : value;
    }
    return masked;
}
function tryGet(app, token) {
    try {
        return app.get(token);
    }
    catch {
        return undefined;
    }
}
function createReplContext(app) {
    const resolve = (token) => app.get(token);
    return {
        app,
        container: app.container,
        config: app.config,
        db: tryGet(app, database_1.BaseDatabase),
        resolve,
        r: resolve,
        $: resolve,
        process,
        Buffer,
        inspect: node_util_1.inspect
    };
}
function completeTopLevel(context, prefix) {
    const globals = [
        ...Object.keys(context),
        'console',
        'process',
        'Buffer',
        'Promise',
        'JSON',
        'Math',
        'Date',
        'Array',
        'Object',
        'String',
        'Number',
        'Boolean',
        'Map',
        'Set',
        'RegExp',
        'Error',
        'undefined',
        'null',
        'true',
        'false',
        'async',
        'await'
    ];
    return [...new Set(globals)]
        .filter(label => label.startsWith(prefix))
        .sort((a, b) => a.localeCompare(b))
        .slice(0, 50)
        .map(label => ({ label, kind: Object.hasOwn(context, label) ? 'context' : 'global' }));
}
function collectProperties(value, prefix) {
    const seen = new Set();
    const items = [];
    let current = value;
    while (current != null) {
        for (const name of Object.getOwnPropertyNames(current)) {
            if (!name.startsWith(prefix) || seen.has(name) || name.startsWith('__'))
                continue;
            seen.add(name);
            let kind = 'property';
            try {
                const descriptor = Object.getOwnPropertyDescriptor(current, name);
                if (typeof descriptor?.value === 'function')
                    kind = 'method';
                else if (descriptor?.get || descriptor?.set)
                    kind = 'accessor';
            }
            catch {
                // Ignore exotic property descriptors.
            }
            items.push({ label: name, kind });
        }
        current = Object.getPrototypeOf(current);
        if (current === Object.prototype)
            break;
    }
    return items.sort((a, b) => a.label.localeCompare(b.label));
}
function bullJobToDevConsoleJob(queueName, job, status) {
    const data = isBullWorkerJobData(job.data) ? job.data.data : job.data;
    return {
        id: `${queueName}:${job.id ?? ''}`,
        queue: queueName,
        queueId: String(job.id ?? ''),
        name: job.name,
        data,
        status,
        attempt: job.attemptsMade,
        createdAt: job.timestamp,
        shouldExecuteAt: job.timestamp + (job.opts.delay ?? 0),
        executedAt: job.processedOn ?? null
    };
}
function isBullWorkerJobData(value) {
    return !!value && typeof value === 'object' && 'data' in value;
}
function recordToWorkerJob(record) {
    return {
        id: record.id,
        queue: record.queue,
        queueId: record.queueId,
        name: record.name,
        data: record.data,
        status: record.status,
        result: record.result,
        attempt: record.attempt,
        traceId: record.traceId,
        createdAt: record.createdAt.getTime(),
        shouldExecuteAt: record.shouldExecuteAt.getTime(),
        executedAt: record.executedAt.getTime(),
        completedAt: record.completedAt.getTime()
    };
}
function stringifyCell(value) {
    if (value === null || value === undefined)
        return '';
    if (value instanceof Date)
        return value.toISOString();
    if (Buffer.isBuffer(value))
        return value.toString('base64');
    return typeof value === 'object' ? (0, helpers_1.safeJsonStringify)(value) : String(value);
}
//# sourceMappingURL=server.js.map