/**
 * Logging utility for mailpouch
 */
import { appendFile, statSync, chmodSync } from "fs";
import { join } from "path";
import { homedir } from "os";
function statSyncSafe(p) {
    try {
        return statSync(p);
    }
    catch {
        return null;
    }
}
function chmodSyncSafe(p, m) {
    try {
        chmodSync(p, m);
    }
    catch { /* ignore */ }
}
/**
 * Keys whose values must be redacted before being stored in log entries.
 * Matched as a substring so compound names like `remoteBearerToken`,
 * `passAccessToken`, `simpleloginApiKey`, `codeVerifier`, `access_token`,
 * `client_secret` are all covered. The first group is "values we never
 * want on disk"; the second is "email body-like fields" that might carry
 * user content we don't need in a debug log.
 */
const SENSITIVE_KEYS = /(password|token|secret|apikey|api_key|verifier|credential|authorization|cookie|oauth|bridgecertpath|attachments|content|^body$)/i;
export function getLogFilePath() {
    const envPath = process.env.MAILPOUCH_LOG_FILE;
    if (envPath)
        return envPath;
    return join(homedir(), ".mailpouch.log");
}
export class Logger {
    debugMode = false;
    logs = [];
    maxLogs = 1000;
    // Becomes true on first setDebugMode() call (i.e. once config is loaded by the server).
    // Tests that never call setDebugMode will not write to the log file.
    fileLogging = false;
    // Gates debug-level file writes specifically.
    debugFileLogging = false;
    /**
     * Recursively sanitize data before storing in log entries.
     * Redacts sensitive keys and truncates long strings.
     *
     * Cap recursion at SANITIZE_MAX_DEPTH so adversary-supplied JSON nested
     * thousands of levels deep can't blow the stack or burn the CPU during
     * sanitisation.
     */
    sanitizeData(data, seen, depth = 0) {
        if (data === null || data === undefined)
            return data;
        if (typeof data === "string") {
            const truncated = data.length > 200 ? data.substring(0, 200) + "…" : data;
            return truncated.replace(/[\x00-\x1f\x7f]/g, " ");
        }
        if (typeof data !== "object")
            return data;
        if (depth > 100)
            return "[depth-limit]";
        // Prevent infinite recursion on circular references (e.g. socket/TLS error objects)
        const tracker = seen ?? new WeakSet();
        if (tracker.has(data))
            return "[circular]";
        tracker.add(data);
        if (Array.isArray(data)) {
            return data.map((item) => this.sanitizeData(item, tracker, depth + 1));
        }
        const result = {};
        for (const [key, value] of Object.entries(data)) {
            result[key] = SENSITIVE_KEYS.test(key) ? "[redacted]" : this.sanitizeData(value, tracker, depth + 1);
        }
        return result;
    }
    setDebugMode(enabled) {
        this.debugMode = enabled;
        // Activate file logging on first call so info/warn/error are always captured
        // once the server has loaded its config. Tests that never call setDebugMode
        // won't write to the log file.
        this.fileLogging = true;
        // debugFileLogging gates whether debug-level entries also go to file.
        this.debugFileLogging = enabled;
    }
    ts() {
        return new Date().toISOString();
    }
    debug(message, context = "System", data) {
        if (this.debugMode) {
            this.log("debug", message, context, data);
            console.error(`${this.ts()} [DEBUG] [${context}] ${message}`, data || "");
        }
    }
    info(message, context = "System", data) {
        this.log("info", message, context, data);
        console.error(`${this.ts()} [INFO] [${context}] ${message}`, data || "");
    }
    warn(message, context = "System", data) {
        this.log("warn", message, context, data);
        console.error(`${this.ts()} [WARN] [${context}] ${message}`, data || "");
    }
    error(message, context = "System", error) {
        this.log("error", message, context, error);
        console.error(`${this.ts()} [ERROR] [${context}] ${message}`, error || "");
    }
    log(level, message, context, data) {
        const entry = {
            timestamp: new Date(),
            level,
            context,
            message,
            data: this.sanitizeData(data),
        };
        this.logs.push(entry);
        // Keep only last N logs to prevent memory issues
        if (this.logs.length > this.maxLogs) {
            this.logs.shift();
        }
        // Append to log file:
        //   - info / warn / error always write once fileLogging is active (set after config loads)
        //   - debug entries only write when debugFileLogging is also enabled
        const shouldWriteFile = this.fileLogging &&
            (level !== "debug" || this.debugFileLogging);
        if (shouldWriteFile) {
            // Explicit mode 0o600 so debug entries (which may carry email-like
            // context, even after redaction) are owner-readable only.
            // appendFile creates the file if missing; otherwise the mode is
            // ignored, so we also chmodSync if the file already exists with
            // looser permissions (e.g. upgraded from an older mailpouch).
            const path = getLogFilePath();
            try {
                const st = statSyncSafe(path);
                if (st && (st.mode & 0o077) !== 0)
                    chmodSyncSafe(path, 0o600);
            }
            catch { /* ignore */ }
            appendFile(path, JSON.stringify(entry) + "\n", { encoding: "utf8", mode: 0o600 }, () => {
                /* best-effort — never crash the server over a log write */
            });
        }
    }
    getLogs(level, limit = 100) {
        const safeLimit = Math.min(Math.max(1, limit), 500);
        let filteredLogs = this.logs;
        if (level) {
            filteredLogs = filteredLogs.filter((log) => log.level === level);
        }
        return filteredLogs.slice(-safeLimit);
    }
    clearLogs() {
        this.logs = [];
    }
}
// Export singleton instance
export const logger = new Logger();
//# sourceMappingURL=logger.js.map