import * as plugins from '../../plugins.js';
import * as interfaces from '../../../dist_ts_interfaces/index.js';
import { logBuffer, baseLogger } from '../../logger.js';
import { requireOpsAuth } from '../helpers/auth.js';
// Module-level singleton: the log push destination is added once and reuses
// the current OpsServer reference so it survives OpsServer restarts without
// accumulating duplicate destinations.
let logPushDestinationInstalled = false;
let currentOpsServerRef = null;
export class LogsHandler {
    opsServerRef;
    activeStreamStops = new Set();
    constructor(opsServerRef) {
        this.opsServerRef = opsServerRef;
        this.registerHandlers();
        this.setupLogPushDestination();
    }
    /**
     * Clean up all active log streams and deactivate the push destination.
     * Called when OpsServer stops.
     */
    cleanup() {
        // Stop all active follow-mode log streams
        for (const stop of this.activeStreamStops) {
            stop();
        }
        this.activeStreamStops.clear();
        // Deactivate the push destination (it stays registered but becomes a no-op)
        currentOpsServerRef = null;
    }
    registerHandlers() {
        // All log endpoints register directly on viewRouter (valid identity required via middleware)
        const router = this.opsServerRef.viewRouter;
        // Get Recent Logs Handler
        router.addTypedHandler(new plugins.typedrequest.TypedHandler('getRecentLogs', async (dataArg, toolsArg) => {
            await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'logs:read' });
            const logs = await this.getRecentLogs(dataArg.level, dataArg.category, dataArg.limit || 100, dataArg.offset || 0, dataArg.search, dataArg.timeRange);
            return {
                logs,
                total: logs.length,
                hasMore: false,
            };
        }));
        // Get Log Stream Handler
        router.addTypedHandler(new plugins.typedrequest.TypedHandler('getLogStream', async (dataArg, toolsArg) => {
            await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'logs:read' });
            // Create a virtual stream for log streaming
            const virtualStream = new plugins.typedrequest.VirtualStream();
            // Set up log streaming
            const streamLogs = this.setupLogStream(virtualStream, dataArg.filters?.level, dataArg.filters?.category, dataArg.follow);
            // Start streaming
            streamLogs.start();
            // Track the stop function so we can clean up on shutdown
            this.activeStreamStops.add(streamLogs.stop);
            return {
                logStream: virtualStream,
            };
        }));
    }
    static mapLogLevel(smartlogLevel) {
        switch (smartlogLevel) {
            case 'silly':
            case 'debug':
                return 'debug';
            case 'warn':
                return 'warn';
            case 'error':
                return 'error';
            default:
                return 'info';
        }
    }
    static deriveCategory(zone, message) {
        const msg = (message || '').toLowerCase();
        if (msg.includes('[security:') || msg.includes('security'))
            return 'security';
        if (zone === 'email' || msg.includes('email') || msg.includes('smtp') || msg.includes('mta'))
            return 'email';
        if (zone === 'dns' || msg.includes('dns'))
            return 'dns';
        if (msg.includes('smtp'))
            return 'smtp';
        return 'system';
    }
    async getRecentLogs(level, category, limit = 100, offset = 0, search, timeRange) {
        // Compute a timestamp cutoff from timeRange
        let since;
        if (timeRange) {
            const rangeMs = {
                '1h': 3600000,
                '6h': 21600000,
                '24h': 86400000,
                '7d': 604800000,
                '30d': 2592000000,
            };
            since = Date.now() - (rangeMs[timeRange] || 86400000);
        }
        // Map the UI level to smartlog levels for filtering
        const smartlogLevels = level
            ? level === 'debug'
                ? ['debug', 'silly']
                : level === 'info'
                    ? ['info', 'ok', 'success', 'note', 'lifecycle']
                    : [level]
            : undefined;
        // Fetch a larger batch from buffer, then apply category filter client-side
        const rawEntries = logBuffer.getEntries({
            level: smartlogLevels,
            search,
            since,
            limit: limit * 3, // over-fetch to compensate for category filtering
            offset: 0,
        });
        // Map ILogPackage → UI log format and apply category filter
        const mapped = [];
        for (const pkg of rawEntries) {
            const uiLevel = LogsHandler.mapLogLevel(pkg.level);
            const uiCategory = LogsHandler.deriveCategory(pkg.context?.zone, pkg.message);
            if (category && uiCategory !== category)
                continue;
            mapped.push({
                timestamp: pkg.timestamp,
                level: uiLevel,
                category: uiCategory,
                message: pkg.message,
                metadata: pkg.data,
            });
            if (mapped.length >= limit)
                break;
        }
        return mapped;
    }
    /**
     * Add a log destination to the base logger that pushes entries
     * to all connected ops_dashboard TypedSocket clients.
     *
     * Uses a module-level singleton so the destination is added only once,
     * even across OpsServer restart cycles. The destination reads
     * `currentOpsServerRef` dynamically so it always uses the active server.
     */
    setupLogPushDestination() {
        // Update the module-level reference so the existing destination uses the new server
        currentOpsServerRef = this.opsServerRef;
        if (logPushDestinationInstalled) {
            return; // destination already registered — just updated the ref
        }
        logPushDestinationInstalled = true;
        baseLogger.addLogDestination({
            async handleLog(logPackage) {
                const opsServer = currentOpsServerRef;
                if (!opsServer)
                    return;
                const typedsocket = opsServer.server?.typedserver?.typedsocket;
                if (!typedsocket)
                    return;
                let connections;
                try {
                    connections = await typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard');
                }
                catch {
                    return;
                }
                if (connections.length === 0)
                    return;
                const entry = {
                    timestamp: logPackage.timestamp || Date.now(),
                    level: LogsHandler.mapLogLevel(logPackage.level),
                    category: LogsHandler.deriveCategory(logPackage.context?.zone, logPackage.message),
                    message: logPackage.message,
                    metadata: logPackage.data,
                };
                for (const conn of connections) {
                    try {
                        const push = typedsocket.createTypedRequest('pushLogEntry', conn);
                        push.fire({ entry }).catch(() => { }); // fire-and-forget
                    }
                    catch {
                        // connection may have closed
                    }
                }
            },
        });
    }
    setupLogStream(virtualStream, levelFilter, categoryFilter, follow = true) {
        let intervalId = null;
        let stopped = false;
        let lastTimestamp = Date.now();
        const stop = () => {
            stopped = true;
            if (intervalId) {
                clearInterval(intervalId);
                intervalId = null;
            }
            this.activeStreamStops.delete(stop);
        };
        const start = () => {
            if (!follow) {
                // Send existing logs and close
                this.getRecentLogs(levelFilter?.[0], categoryFilter?.[0], 100, 0).then(logs => {
                    logs.forEach(log => {
                        const logData = JSON.stringify(log);
                        const encoder = new TextEncoder();
                        virtualStream.sendData(encoder.encode(logData));
                    });
                });
                return;
            }
            // For follow mode, tail real log entries from the in-memory buffer
            intervalId = setInterval(async () => {
                if (stopped) {
                    clearInterval(intervalId);
                    intervalId = null;
                    return;
                }
                // Fetch new entries since last poll
                const rawEntries = logBuffer.getEntries({
                    since: lastTimestamp,
                    limit: 50,
                });
                if (rawEntries.length === 0)
                    return;
                for (const raw of rawEntries) {
                    const mappedLevel = LogsHandler.mapLogLevel(raw.level);
                    const mappedCategory = LogsHandler.deriveCategory(raw.context?.zone, raw.message);
                    // Apply filters
                    if (levelFilter && !levelFilter.includes(mappedLevel))
                        continue;
                    if (categoryFilter && !categoryFilter.includes(mappedCategory))
                        continue;
                    const logEntry = {
                        timestamp: raw.timestamp || Date.now(),
                        level: mappedLevel,
                        category: mappedCategory,
                        message: raw.message,
                        metadata: raw.data,
                    };
                    const logData = JSON.stringify(logEntry);
                    const encoder = new TextEncoder();
                    try {
                        let timeoutHandle;
                        await Promise.race([
                            virtualStream.sendData(encoder.encode(logData)).then((result) => {
                                clearTimeout(timeoutHandle);
                                return result;
                            }),
                            new Promise((_, reject) => {
                                timeoutHandle = setTimeout(() => reject(new Error('stream send timeout')), 10_000);
                            }),
                        ]);
                    }
                    catch {
                        // Stream closed, errored, or timed out — clean up
                        stop();
                        return;
                    }
                }
                // Advance the watermark past all entries we just processed
                const newest = rawEntries[rawEntries.length - 1];
                if (newest.timestamp && newest.timestamp >= lastTimestamp) {
                    lastTimestamp = newest.timestamp + 1;
                }
            }, 2000);
        };
        return { start, stop };
    }
}
//# sourceMappingURL=data:application/json;base64, 