/**
 * Storage Service
 * Unified service for secure file operations, user uploads, and internal data storage
 * Provides path sanitization, validation, and consistent file handling
 */
import path from "path";
import * as fsUtils from "../utils/fs-utils.js";
import { createWriteStream } from "fs";
import { createLogger } from "./index.js";
import crypto from "crypto";
let logger; // Will be initialized when needed
function ensureLogger() {
    if (!logger) {
        logger = createLogger("StorageService");
    }
    return logger;
}
// Define safe base directories
const DATA_DIR = path.join(process.cwd(), "data");
const BASE_DIRS = {
    attachments: path.join(DATA_DIR, "uploads", "attachments"),
    data: DATA_DIR,
    templates: path.join(DATA_DIR, "templates"),
    uploads: path.join(DATA_DIR, "uploads"),
    logs: path.join(DATA_DIR, "logs"),
    config: path.join(DATA_DIR, "config"),
};
export class StorageService {
    initialized = false;
    maxFileSize = 100 * 1024 * 1024; // 100MB
    async initialize() {
        if (this.initialized)
            return;
        try {
            // Ensure all base directories exist
            for (const dir of Object.values(BASE_DIRS)) {
                await fsUtils.ensureDir(dir);
            }
            // Ensure .gitignore exists in attachments and data directories
            const gitignoreContent = "*\n!.gitignore\n";
            await fsUtils.writeFile(path.join(BASE_DIRS.attachments, ".gitignore"), gitignoreContent);
            await fsUtils.writeFile(path.join(BASE_DIRS.data, ".gitignore"), gitignoreContent);
            this.initialized = true;
            // Storage service initialized
        }
        catch (_error) {
            ensureLogger().error("Failed to initialize storage service:", _error);
            throw _error;
        }
    }
    /**
     * Sanitize a filename to prevent path traversal attacks
     */
    sanitizeFilename(filename) {
        if (!filename || typeof filename !== "string") {
            throw new Error("Invalid filename provided");
        }
        // Remove any path components and keep only the basename
        let sanitized = path.basename(filename);
        // Remove any remaining special characters that could cause issues
        sanitized = sanitized.replace(/[^a-zA-Z0-9._-]/g, "_");
        // Ensure the filename is not empty after sanitization
        if (!sanitized || sanitized === "." || sanitized === "..") {
            sanitized = `file_${Date.now()}`;
        }
        // Limit filename length
        if (sanitized.length > 255) {
            const ext = path.extname(sanitized);
            const name = path.basename(sanitized, ext);
            sanitized = name.substring(0, 255 - ext.length) + ext;
        }
        return sanitized;
    }
    /**
     * Validate that a path is within the allowed base directory
     */
    isPathSafe(filePath, baseDir) {
        try {
            const resolvedPath = path.resolve(filePath);
            const resolvedBase = path.resolve(baseDir);
            return resolvedPath.startsWith(resolvedBase);
        }
        catch (_error) {
            ensureLogger().error("Error validating path safety:", _error);
            return false;
        }
    }
    /**
     * Get the full safe path for a file
     */
    getSafePath(filename, category = "data") {
        const sanitizedName = this.sanitizeFilename(filename);
        const baseDir = BASE_DIRS[category];
        const fullPath = path.join(baseDir, sanitizedName);
        if (!this.isPathSafe(fullPath, baseDir)) {
            throw new Error("Invalid file path detected");
        }
        return fullPath;
    }
    /**
     * Save a file securely
     */
    async saveFile(content, filename, category = "data", options = {}) {
        await this.initialize();
        const safePath = this.getSafePath(filename, category);
        // Check if file exists and handle accordingly
        if (await fsUtils.pathExists(safePath)) {
            if (!options.overwrite) {
                throw new Error(`File already exists: ${filename}`);
            }
            if (options.createBackup) {
                const backupPath = `${safePath}.backup.${Date.now()}`;
                await fsUtils.copy(safePath, backupPath);
                ensureLogger().info(`Created backup: ${backupPath}`);
            }
        }
        // Validate content size
        const size = Buffer.isBuffer(content)
            ? content.length
            : Buffer.byteLength(content);
        if (size > this.maxFileSize) {
            throw new Error(`File size exceeds maximum allowed size of ${this.maxFileSize} bytes`);
        }
        // Save the file
        await fsUtils.writeFile(safePath, content);
        // Calculate hash
        const hash = crypto
            .createHash("sha256")
            .update(Buffer.isBuffer(content) ? content : Buffer.from(content))
            .digest("hex");
        const stats = await fsUtils.stat(safePath);
        ensureLogger().debug(`File saved: ${safePath} (${size} bytes)`);
        return {
            name: filename,
            size,
            path: safePath,
            hash,
            created: stats.birthtime,
            modified: stats.mtime,
        };
    }
    /**
     * Read a file securely
     */
    async readFile(filename, category = "data", options = {}) {
        await this.initialize();
        const safePath = this.getSafePath(filename, category);
        // Check if file exists
        if (!(await fsUtils.pathExists(safePath))) {
            throw new Error(`File not found: ${filename}`);
        }
        // Check file size
        const stats = await fsUtils.stat(safePath);
        const maxSize = options.maxSize || this.maxFileSize;
        if (stats.size > maxSize) {
            throw new Error(`File size (${stats.size}) exceeds maximum allowed size (${maxSize})`);
        }
        // Read the file
        const content = options.encoding
            ? await fsUtils.readFile(safePath, options.encoding)
            : await fsUtils.readFile(safePath);
        ensureLogger().debug(`File read: ${safePath} (${stats.size} bytes)`);
        return content;
    }
    /**
     * Delete a file securely
     */
    async deleteFile(filename, category = "data") {
        await this.initialize();
        const safePath = this.getSafePath(filename, category);
        if (!(await fsUtils.pathExists(safePath))) {
            ensureLogger().warn(`File not found for deletion: ${filename}`);
            return false;
        }
        await fsUtils.unlink(safePath);
        ensureLogger().debug(`File deleted: ${safePath}`);
        return true;
    }
    /**
     * List files in a category
     */
    async listFiles(category = "data", pattern) {
        await this.initialize();
        const baseDir = BASE_DIRS[category];
        const files = [];
        try {
            const entries = await fsUtils.readdir(baseDir);
            for (const entry of entries) {
                if (pattern && !pattern.test(entry))
                    continue;
                const fullPath = path.join(baseDir, entry);
                const stats = await fsUtils.stat(fullPath);
                if (stats.isFile()) {
                    files.push({
                        name: entry,
                        size: stats.size,
                        path: fullPath,
                        created: stats.birthtime,
                        modified: stats.mtime,
                    });
                }
            }
        }
        catch (_error) {
            ensureLogger().error(`Error listing files in ${category}:`, _error);
        }
        return files;
    }
    /**
     * Get file info
     */
    async getFileInfo(filename, category = "data") {
        await this.initialize();
        const safePath = this.getSafePath(filename, category);
        if (!(await fsUtils.pathExists(safePath))) {
            return null;
        }
        const stats = await fsUtils.stat(safePath);
        return {
            name: filename,
            size: stats.size,
            path: safePath,
            created: stats.birthtime,
            modified: stats.mtime,
        };
    }
    /**
     * Move file between categories
     */
    async moveFile(filename, fromCategory, toCategory) {
        await this.initialize();
        const sourcePath = this.getSafePath(filename, fromCategory);
        const destPath = this.getSafePath(filename, toCategory);
        if (!(await fsUtils.pathExists(sourcePath))) {
            throw new Error(`Source file not found: ${filename}`);
        }
        await fsUtils.move(sourcePath, destPath, { overwrite: false });
        const stats = await fsUtils.stat(destPath);
        ensureLogger().debug(`File moved from ${fromCategory} to ${toCategory}: ${filename}`);
        return {
            name: filename,
            size: stats.size,
            path: destPath,
            created: stats.birthtime,
            modified: stats.mtime,
        };
    }
    /**
     * Get available base directories
     */
    getBaseDirectories() {
        return { ...BASE_DIRS };
    }
    /**
     * Save a user upload with security checks
     * @param file - File buffer or stream
     * @param originalName - Original filename from user
     * @param options - Upload options
     * @returns Information about the uploaded file
     */
    async saveUserUpload(file, originalName, options = {}) {
        await this.initialize();
        const { maxSize = this.maxFileSize, allowedTypes = [], destination = "uploads", generateUniqueName = true, preserveExtension = true, } = options;
        // Sanitize the original filename
        const sanitizedName = this.sanitizeFilename(originalName);
        const ext = path.extname(sanitizedName).toLowerCase();
        const nameWithoutExt = path.basename(sanitizedName, ext);
        // Check file extension if restrictions are specified
        if (allowedTypes.length > 0 && !allowedTypes.includes(ext)) {
            throw new Error(`File type ${ext} is not allowed. Allowed types: ${allowedTypes.join(", ")}`);
        }
        // Generate filename
        let filename;
        if (generateUniqueName) {
            const uniqueId = crypto.randomBytes(8).toString("hex");
            filename = preserveExtension
                ? `${nameWithoutExt}_${uniqueId}${ext}`
                : `${uniqueId}`;
        }
        else {
            filename = sanitizedName;
        }
        // Determine the upload path. Prefer known base directories, but allow custom
        // destinations for backward compatibility with the old FileHandler API.
        let uploadPath;
        if (BASE_DIRS[destination]) {
            uploadPath = this.getSafePath(filename, destination);
        }
        else {
            const resolvedDest = path.isAbsolute(destination)
                ? destination
                : path.join(process.cwd(), destination);
            await fsUtils.ensureDir(resolvedDest);
            uploadPath = path.join(resolvedDest, this.sanitizeFilename(filename));
        }
        // Save the file
        if (Buffer.isBuffer(file)) {
            // Check size for buffer
            if (file.length > maxSize) {
                throw new Error(`File size exceeds maximum allowed size of ${maxSize} bytes`);
            }
            await fsUtils.writeFile(uploadPath, file);
        }
        else {
            // For streams, we need to check size while writing
            const writeStream = createWriteStream(uploadPath);
            let size = 0;
            await new Promise((resolve, reject) => {
                file.on("data", (chunk) => {
                    size += chunk.length;
                    if (size > maxSize) {
                        writeStream.destroy();
                        fsUtils.unlink(uploadPath).catch(() => { }); // Clean up partial file
                        reject(new Error(`File size exceeds maximum allowed size of ${maxSize} bytes`));
                    }
                });
                file.on("error", reject);
                writeStream.on("error", reject);
                writeStream.on("finish", () => resolve(undefined));
                file.pipe(writeStream);
            });
        }
        // Generate file hash
        const fileBuffer = await fsUtils.readFile(uploadPath);
        const hash = crypto.createHash("sha256").update(fileBuffer).digest("hex");
        // Get file stats
        const stats = await fsUtils.stat(uploadPath);
        const uploadedFile = {
            originalName,
            filename,
            path: uploadPath,
            size: stats.size,
            extension: ext,
            hash,
        };
        ensureLogger().info("User file uploaded successfully", {
            originalName,
            filename,
            size: stats.size,
            hash,
        });
        return uploadedFile;
    }
    /**
     * Clean temporary files older than a given age. Falls back to ./temp to
     * preserve the previous middleware behaviour.
     */
    async cleanTempFiles(maxAgeMs = 24 * 60 * 60 * 1000, tempDir = path.join(process.cwd(), "temp")) {
        await fsUtils.ensureDir(tempDir);
        const now = Date.now();
        let deletedCount = 0;
        try {
            const files = await fsUtils.readdir(tempDir);
            for (const file of files) {
                const filePath = path.join(tempDir, file);
                const stats = await fsUtils.stat(filePath);
                if (!stats.isFile())
                    continue;
                if (now - stats.mtimeMs > maxAgeMs) {
                    await fsUtils.unlink(filePath);
                    deletedCount++;
                }
            }
            if (deletedCount > 0) {
                ensureLogger().info(`Cleaned ${deletedCount} temporary file(s) older than ${Math.round(maxAgeMs / (1000 * 60 * 60))}h`);
            }
        }
        catch (_error) {
            ensureLogger().error("Error cleaning temp files:", _error);
        }
        return deletedCount;
    }
    /**
     * Save internal application data
     * @param data - Data to save
     * @param filename - Filename for the data
     * @param category - Category/directory for the data
     * @param options - Save options
     */
    async saveInternalData(data, filename, category = "data", options = {}) {
        // If data is an object, stringify it
        const finalData = typeof data === "object" && !Buffer.isBuffer(data)
            ? JSON.stringify(data, null, 2)
            : data;
        // Use the existing save method
        return this.saveFile(finalData, filename, category, options);
    }
}
// Singleton instance
let storageService = null;
export function getStorageService() {
    if (!storageService) {
        storageService = new StorageService();
    }
    return storageService;
}
// Backward compatibility export (deprecated)
export function getSecureFileHandler() {
    console.warn("getSecureFileHandler is deprecated. Use getStorageService instead.");
    return getStorageService();
}
// Export for backward compatibility (deprecated)
export { StorageService as SecureFileHandler };
export default StorageService;
//# sourceMappingURL=storageService.js.map