"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ensureArchiveDir = ensureArchiveDir;
exports.generateArchiveFilename = generateArchiveFilename;
exports.listArchiveFiles = listArchiveFiles;
exports.deleteArchiveFile = deleteArchiveFile;
exports.acquireFileLock = acquireFileLock;
exports.releaseFileLock = releaseFileLock;
exports.calculateFileChecksum = calculateFileChecksum;
exports.verifyArchiveIntegrity = verifyArchiveIntegrity;
exports.checkDiskSpace = checkDiskSpace;
exports.cleanupOldArchives = cleanupOldArchives;
const crypto = __importStar(require("node:crypto"));
const fs = __importStar(require("node:fs/promises"));
const path = __importStar(require("node:path"));
const proper_lockfile_1 = __importDefault(require("proper-lockfile"));
/**
 * Check if a path contains path traversal attempts
 * This checks if the path contains ".." components before normalization
 */
function containsPathTraversal(filePath) {
    // Check for ".." in the path string
    return filePath.includes("..");
}
/**
 * Ensure archive directory exists, create if needed
 * @param dirPath - Absolute path to archive directory
 */
async function ensureArchiveDir(dirPath) {
    // Validate absolute path
    if (!path.isAbsolute(dirPath)) {
        throw new Error(`Archive directory must be an absolute path: ${dirPath}`);
    }
    // Check for path traversal
    if (containsPathTraversal(dirPath)) {
        throw new Error(`Path traversal detected in archive directory: ${dirPath}`);
    }
    // Check if path exists
    try {
        const stats = await fs.stat(dirPath);
        if (!stats.isDirectory()) {
            throw new Error(`Path exists but is not a directory: ${dirPath}`);
        }
        // Directory exists, return
        return;
    }
    catch (error) {
        if (error.code !== "ENOENT") {
            // Some other error
            throw error;
        }
        // Directory doesn't exist, create it
    }
    // Create directory recursively
    try {
        await fs.mkdir(dirPath, { recursive: true, mode: 0o755 });
    }
    catch (error) {
        throw new Error(`Failed to create archive directory: ${error.message}`);
    }
}
/**
 * Generate archive filename from date with timestamp
 * Format: observability-archive-YYYY-MM-DD-{timestamp}.db
 */
function generateArchiveFilename(date, timestamp) {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, "0");
    const day = String(date.getDate()).padStart(2, "0");
    const ts = timestamp ?? Date.now();
    return `observability-archive-${year}-${month}-${day}-${ts}.db`;
}
/**
 * List all archive files in directory, sorted by date (newest first)
 * Only returns files matching the archive filename pattern
 */
async function listArchiveFiles(dirPath) {
    try {
        const entries = await fs.readdir(dirPath, { withFileTypes: true });
        const archiveFiles = entries
            .filter((entry) => {
            // Only include files (not directories)
            if (!entry.isFile()) {
                return false;
            }
            // Only include files matching the archive pattern (with optional timestamp suffix)
            return entry.name.match(/^observability-archive-\d{4}-\d{2}-\d{2}(-\d+)?\.db$/);
        })
            .map((entry) => entry.name)
            .sort((a, b) => {
            // Sort by date in filename (newest first)
            return b.localeCompare(a);
        });
        return archiveFiles;
    }
    catch (error) {
        if (error.code === "ENOENT") {
            // Directory doesn't exist, return empty array
            return [];
        }
        throw error;
    }
}
/**
 * Delete an archive file with safety checks
 * @param filePath - Absolute path to archive file
 */
async function deleteArchiveFile(filePath) {
    // Validate it's an absolute path
    if (!path.isAbsolute(filePath)) {
        throw new Error(`Archive file path must be absolute: ${filePath}`);
    }
    // Check for path traversal
    if (containsPathTraversal(filePath)) {
        throw new Error(`Path traversal detected in archive file path: ${filePath}`);
    }
    // Additional safety: ensure filename matches archive pattern (with optional timestamp suffix)
    const filename = path.basename(filePath);
    if (!filename.match(/^observability-archive-\d{4}-\d{2}-\d{2}(-\d+)?\.db$/)) {
        throw new Error(`File does not match archive filename pattern: ${filename}`);
    }
    // Check if file exists
    try {
        const stats = await fs.lstat(filePath); // Use lstat to detect symlinks
        // Reject symlinks
        if (stats.isSymbolicLink()) {
            throw new Error(`Cannot delete symlink: ${filePath}`);
        }
        // Only delete files
        if (!stats.isFile()) {
            throw new Error(`Path is not a file: ${filePath}`);
        }
    }
    catch (error) {
        if (error.code === "ENOENT") {
            // File doesn't exist, no-op
            return;
        }
        throw error;
    }
    // Delete the file
    try {
        await fs.unlink(filePath);
    }
    catch (error) {
        throw new Error(`Failed to delete archive file: ${error.message}`);
    }
}
/**
 * Acquire file lock to prevent concurrent archive operations
 * @param lockPath - Path to lock file
 * @returns Release function, or null if lock is already held
 */
async function acquireFileLock(lockPath) {
    try {
        const release = await proper_lockfile_1.default.lock(lockPath, {
            retries: 0, // Don't retry, fail immediately
            stale: 60000, // 60 seconds stale lock timeout
            realpath: false, // Don't resolve symlinks
        });
        return release;
    }
    catch (error) {
        if (error.code === "ELOCKED") {
            // Lock is held by another process
            return null;
        }
        throw error;
    }
}
/**
 * Release file lock
 * @param release - Release function returned by acquireFileLock
 */
async function releaseFileLock(release) {
    await release();
}
/**
 * Calculate SHA256 checksum for a file
 * @param filePath - Absolute path to file
 * @returns Hex-encoded SHA256 checksum
 */
async function calculateFileChecksum(filePath) {
    return new Promise((resolve, reject) => {
        const hash = crypto.createHash("sha256");
        const stream = require("node:fs").createReadStream(filePath);
        stream.on("data", (chunk) => hash.update(chunk));
        stream.on("end", () => resolve(hash.digest("hex")));
        stream.on("error", (error) => reject(error));
    });
}
/**
 * Verify archive file integrity using checksum
 * @param archivePath - Absolute path to archive file
 * @param expectedChecksum - Expected SHA256 checksum
 * @returns True if checksum matches, false otherwise
 */
async function verifyArchiveIntegrity(archivePath, expectedChecksum) {
    try {
        const actualChecksum = await calculateFileChecksum(archivePath);
        return actualChecksum === expectedChecksum;
    }
    catch (_error) {
        // File doesn't exist or can't be read
        return false;
    }
}
/**
 * Check available disk space in bytes
 * @param dirPath - Directory path to check
 * @returns Available space in bytes
 */
async function checkDiskSpace(dirPath) {
    try {
        const stats = await fs.statfs(dirPath);
        return stats.bavail * stats.bsize; // Available bytes
    }
    catch (error) {
        throw new Error(`Failed to check disk space: ${error.message}`);
    }
}
/**
 * Clean up old archive files, keeping only the newest maxFiles
 * @param archivesDir - Archive directory path
 * @param maxFiles - Maximum number of archive files to keep
 * @returns List of deleted archive filenames
 */
async function cleanupOldArchives(archivesDir, maxFiles) {
    try {
        // List all archive files
        const archiveFiles = await listArchiveFiles(archivesDir);
        // If we have fewer files than maxFiles, nothing to delete
        if (archiveFiles.length <= maxFiles) {
            return [];
        }
        // Delete oldest files (archives are sorted newest first)
        const filesToDelete = archiveFiles.slice(maxFiles);
        const deletedFiles = [];
        for (const filename of filesToDelete) {
            const filePath = path.join(archivesDir, filename);
            await deleteArchiveFile(filePath);
            deletedFiles.push(filename);
        }
        return deletedFiles;
    }
    catch (error) {
        if (error.code === "ENOENT") {
            // Directory doesn't exist, nothing to clean up
            return [];
        }
        throw error;
    }
}
