"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MongoLogManager = void 0;
const path_1 = __importDefault(require("path"));
const bson_1 = require("bson");
const events_1 = require("events");
const fs_1 = require("fs");
const zlib_1 = require("zlib");
const heap_js_1 = require("heap-js");
const mongo_log_writer_1 = require("./mongo-log-writer");
const stream_1 = require("stream");
class MongoLogManager {
    _options;
    constructor(options) {
        if (options.prefix) {
            if (!/^[a-z0-9_]+$/i.test(options.prefix)) {
                throw new Error('Prefix must only contain letters, numbers, and underscores');
            }
        }
        this._options = options;
    }
    async deleteFile(path) {
        try {
            await fs_1.promises.unlink(path);
        }
        catch (err) {
            if (err?.code !== 'ENOENT') {
                this._options.onerror(err, path);
            }
        }
    }
    get prefix() {
        return this._options.prefix ?? '';
    }
    async cleanupOldLogFiles(maxDurationMs = 5_000, remainingRetries = 1) {
        const deletionStartTimestamp = Date.now();
        const deletionCutoffTimestamp = deletionStartTimestamp - this._options.retentionDays * 86400 * 1000;
        const dir = this._options.directory;
        let dirHandle;
        try {
            dirHandle = await fs_1.promises.opendir(dir);
        }
        catch {
            return;
        }
        const leastRecentFileHeap = new heap_js_1.Heap((a, b) => a.fileTimestamp - b.fileTimestamp);
        const hasRetentionGB = !!this._options.retentionGB && isFinite(this._options.retentionGB);
        const hasMaxLogFileCount = !!this._options.maxLogFileCount &&
            isFinite(this._options.maxLogFileCount);
        let usedStorageSize = hasRetentionGB ? 0 : -Infinity;
        try {
            for await (const dirent of dirHandle) {
                if (Date.now() - deletionStartTimestamp > maxDurationMs)
                    break;
                if (!dirent.isFile())
                    continue;
                const logRegExp = new RegExp(`^${this.prefix}(?<id>[a-f0-9]{24})_log(\\.gz)?$`, 'i');
                const { id } = logRegExp.exec(dirent.name)?.groups ?? {};
                if (!id)
                    continue;
                const fileTimestamp = +new bson_1.ObjectId(id).getTimestamp();
                const fullPath = path_1.default.join(dir, dirent.name);
                if (fileTimestamp < deletionCutoffTimestamp) {
                    await this.deleteFile(fullPath);
                    continue;
                }
                let fileSize;
                if (hasRetentionGB) {
                    try {
                        fileSize = (await fs_1.promises.stat(fullPath)).size;
                        usedStorageSize += fileSize;
                    }
                    catch (err) {
                        this._options.onerror(err, fullPath);
                        continue;
                    }
                }
                if (hasMaxLogFileCount || hasRetentionGB) {
                    leastRecentFileHeap.push({ fullPath, fileTimestamp, fileSize });
                }
                if (hasMaxLogFileCount &&
                    this._options.maxLogFileCount &&
                    leastRecentFileHeap.size() > this._options.maxLogFileCount) {
                    const toDelete = leastRecentFileHeap.pop();
                    if (!toDelete)
                        continue;
                    await this.deleteFile(toDelete.fullPath);
                    usedStorageSize -= toDelete.fileSize ?? 0;
                }
            }
        }
        catch (statErr) {
            if (statErr.code === 'ENOENT' && remainingRetries > 0) {
                await this.cleanupOldLogFiles(maxDurationMs - (Date.now() - deletionStartTimestamp), remainingRetries - 1);
            }
        }
        if (hasRetentionGB && this._options.retentionGB) {
            const storageSizeLimit = this._options.retentionGB * 1024 * 1024 * 1024;
            for (const file of leastRecentFileHeap) {
                if (Date.now() - deletionStartTimestamp > maxDurationMs)
                    break;
                if (usedStorageSize <= storageSizeLimit)
                    break;
                if (!file.fileSize)
                    continue;
                await this.deleteFile(file.fullPath);
                usedStorageSize -= file.fileSize;
            }
        }
    }
    async createLogWriter() {
        const logId = new bson_1.ObjectId().toString();
        const doGzip = !!this._options.gzip;
        const logFilePath = path_1.default.join(this._options.directory, `${this.prefix}${logId}_log${doGzip ? '.gz' : ''}`);
        let originalTarget;
        let stream;
        let logWriter;
        try {
            stream = (0, fs_1.createWriteStream)(logFilePath, { mode: 0o600 });
            originalTarget = stream;
            await (0, events_1.once)(stream, 'ready');
            if (doGzip) {
                stream = (0, zlib_1.createGzip)({
                    flush: zlib_1.constants.Z_SYNC_FLUSH,
                    level: zlib_1.constants.Z_MAX_LEVEL,
                });
                stream.pipe(originalTarget);
            }
            else {
                stream.on('finish', () => stream.emit('log-finish'));
            }
        }
        catch (err) {
            this._options.onwarn(err, logFilePath);
            stream = new stream_1.Writable({
                write(chunk, enc, cb) {
                    cb();
                },
            });
            originalTarget = stream;
            logWriter = new mongo_log_writer_1.MongoLogWriter(logId, null, stream);
        }
        if (!logWriter) {
            logWriter = new mongo_log_writer_1.MongoLogWriter(logId, logFilePath, stream);
        }
        originalTarget.on('finish', () => logWriter?.emit('log-finish'));
        return logWriter;
    }
}
exports.MongoLogManager = MongoLogManager;
//# sourceMappingURL=mongo-log-manager.js.map