/**
 * Secure Temporary File Management
 * Provides secure temporary file operations with proper permissions and cleanup
 */

import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { promisify } from 'util';
import { resourceManager } from './resourceManager';

export interface SecureTempFileOptions {
	prefix?: string;
	suffix?: string;
	directory?: string;
	mode?: number;
	maxAge?: number; // milliseconds before auto-cleanup
	keepOnExit?: boolean; // if false, cleanup on process exit
}

export interface SecureTempFileInfo {
	path: string;
	cleanup: () => Promise<void>;
	isValid: () => boolean;
	getStats: () => fs.Stats | null;
}

export class SecureTempFileManager {
	private static readonly DEFAULT_TEMP_DIR = process.platform === 'win32' 
		? path.join(process.env.TEMP || process.env.TMP || 'C:\\tmp', 'n8n-secure')
		: '/var/tmp/n8n-secure';
	
	private static readonly DEFAULT_MODE = 0o600; // Read/write for owner only
	private static readonly DEFAULT_DIR_MODE = 0o700; // Full access for owner only
	private static readonly MAX_TEMP_FILES = 1000; // Prevent DoS
	private static readonly DEFAULT_MAX_AGE = 30 * 60 * 1000; // 30 minutes

	private static activeTempFiles = new Map<string, SecureTempFileInfo>();
	private static cleanupScheduled = false;

	/**
	 * Create a secure temporary file with proper permissions
	 */
	public static async createSecureTempFile(
		content: string | Buffer,
		options: SecureTempFileOptions = {}
	): Promise<SecureTempFileInfo> {
		try {
			// Validate input
			if (content == null) {
				throw new Error('Content cannot be null or undefined');
			}

			if (typeof content === 'string' && content.length > 10 * 1024 * 1024) {
				throw new Error('Content too large (max 10MB)');
			}

			if (Buffer.isBuffer(content) && content.length > 10 * 1024 * 1024) {
				throw new Error('Content too large (max 10MB)');
			}

			// Check active file limit
			if (this.activeTempFiles.size >= this.MAX_TEMP_FILES) {
				throw new Error('Too many active temporary files - cleanup required');
			}

			// Ensure secure temp directory exists
			const tempDir = options.directory || this.DEFAULT_TEMP_DIR;
			await this.ensureSecureDirectory(tempDir);

			// Generate secure filename
			const filename = this.generateSecureFilename(options.prefix, options.suffix);
			const filepath = path.join(tempDir, filename);

			// Validate the path is within our secure directory
			const resolvedPath = path.resolve(filepath);
			const resolvedDir = path.resolve(tempDir);
			if (!resolvedPath.startsWith(resolvedDir)) {
				throw new Error('Path traversal attempt detected');
			}

			// Write file with secure permissions
			const mode = options.mode || this.DEFAULT_MODE;
			await fs.promises.writeFile(filepath, content, { mode });

			// Verify file was created with correct permissions
			const stats = await fs.promises.stat(filepath);
			if ((stats.mode & 0o777) !== mode) {
				await fs.promises.unlink(filepath);
				throw new Error('Failed to set secure file permissions');
			}

			// Create cleanup function
			let isValid = true;
			const cleanup = async (): Promise<void> => {
				if (!isValid) return;
				
				try {
					await fs.promises.unlink(filepath);
					this.activeTempFiles.delete(filepath);
					isValid = false;
				} catch (error) {
					// File might already be deleted
					if (error.code !== 'ENOENT') {
						console.warn(`Failed to cleanup temp file ${filepath}: ${error.message}`);
					}
					isValid = false;
				}
			};

			// Create file info object
			const fileInfo: SecureTempFileInfo = {
				path: filepath,
				cleanup,
				isValid: () => isValid,
				getStats: () => {
					try {
						return fs.statSync(filepath);
					} catch {
						isValid = false;
						return null;
					}
				}
			};

			// Register for tracking and auto-cleanup
			this.activeTempFiles.set(filepath, fileInfo);

			// Schedule auto-cleanup
			const maxAge = options.maxAge || this.DEFAULT_MAX_AGE;
			setTimeout(async () => {
				if (isValid) {
					await cleanup();
				}
			}, maxAge);

			// Schedule global cleanup if not already done
			if (!this.cleanupScheduled) {
				this.scheduleGlobalCleanup(options.keepOnExit !== true);
			}

			return fileInfo;

		} catch (error) {
			throw new Error(`Failed to create secure temporary file: ${error.message}`);
		}
	}

	/**
	 * Create a secure temporary directory
	 */
	public static async createSecureTempDirectory(
		options: SecureTempFileOptions = {}
	): Promise<{ path: string; cleanup: () => Promise<void> }> {
		try {
			// Ensure secure temp directory exists
			const tempDir = options.directory || this.DEFAULT_TEMP_DIR;
			await this.ensureSecureDirectory(tempDir);

			// Generate secure directory name
			const dirname = this.generateSecureFilename(options.prefix || 'dir', '');
			const dirpath = path.join(tempDir, dirname);

			// Validate the path is within our secure directory
			const resolvedPath = path.resolve(dirpath);
			const resolvedDir = path.resolve(tempDir);
			if (!resolvedPath.startsWith(resolvedDir)) {
				throw new Error('Path traversal attempt detected');
			}

			// Create directory with secure permissions
			const mode = options.mode || this.DEFAULT_DIR_MODE;
			await fs.promises.mkdir(dirpath, { mode });

			// Verify directory was created with correct permissions
			const stats = await fs.promises.stat(dirpath);
			if ((stats.mode & 0o777) !== mode) {
				await fs.promises.rmdir(dirpath);
				throw new Error('Failed to set secure directory permissions');
			}

			// Create cleanup function
			const cleanup = async (): Promise<void> => {
				try {
					await fs.promises.rmdir(dirpath, { recursive: true });
				} catch (error) {
					if (error.code !== 'ENOENT') {
						console.warn(`Failed to cleanup temp directory ${dirpath}: ${error.message}`);
					}
				}
			};

			// Schedule auto-cleanup
			const maxAge = options.maxAge || this.DEFAULT_MAX_AGE;
			setTimeout(cleanup, maxAge);

			return { path: dirpath, cleanup };

		} catch (error) {
			throw new Error(`Failed to create secure temporary directory: ${error.message}`);
		}
	}

	/**
	 * Get a secure temporary directory path (creates if needed)
	 */
	public static async getSecureTempDir(customDir?: string): Promise<string> {
		const tempDir = customDir || this.DEFAULT_TEMP_DIR;
		await this.ensureSecureDirectory(tempDir);
		return tempDir;
	}

	/**
	 * Clean up all active temporary files
	 */
	public static async cleanupAll(): Promise<void> {
		const cleanupPromises = Array.from(this.activeTempFiles.values()).map(
			fileInfo => fileInfo.cleanup()
		);
		
		await Promise.allSettled(cleanupPromises);
		this.activeTempFiles.clear();
	}

	/**
	 * Get statistics about active temporary files
	 */
	public static getStats(): {
		activeFiles: number;
		oldestFileAge: number;
		totalSize: number;
	} {
		let totalSize = 0;
		let oldestFileAge = 0;
		const now = Date.now();

		for (const fileInfo of this.activeTempFiles.values()) {
			if (fileInfo.isValid()) {
				const stats = fileInfo.getStats();
				if (stats) {
					totalSize += stats.size;
					const age = now - stats.birthtimeMs;
					oldestFileAge = Math.max(oldestFileAge, age);
				}
			}
		}

		return {
			activeFiles: this.activeTempFiles.size,
			oldestFileAge,
			totalSize
		};
	}

	/**
	 * Ensure secure directory exists with proper permissions
	 */
	private static async ensureSecureDirectory(dirPath: string): Promise<void> {
		try {
			// Check if directory exists
			const stats = await fs.promises.stat(dirPath);
			
			if (!stats.isDirectory()) {
				throw new Error(`Path exists but is not a directory: ${dirPath}`);
			}

			// Check permissions
			const mode = stats.mode & 0o777;
			if (mode !== this.DEFAULT_DIR_MODE) {
				// Try to fix permissions
				await fs.promises.chmod(dirPath, this.DEFAULT_DIR_MODE);
			}

		} catch (error) {
			if (error.code === 'ENOENT') {
				// Directory doesn't exist, create it
				await fs.promises.mkdir(dirPath, { 
					recursive: true, 
					mode: this.DEFAULT_DIR_MODE 
				});
			} else {
				throw error;
			}
		}
	}

	/**
	 * Generate a secure filename with cryptographic randomness
	 */
	private static generateSecureFilename(prefix?: string, suffix?: string): string {
		const timestamp = Date.now();
		const randomBytes = crypto.randomBytes(16).toString('hex');
		const pid = process.pid;
		
		const parts = [
			prefix || 'secure',
			timestamp,
			pid,
			randomBytes,
			suffix || 'tmp'
		].filter(Boolean);

		return parts.join('_');
	}

	/**
	 * Schedule global cleanup for process exit
	 */
	private static scheduleGlobalCleanup(cleanupOnExit: boolean): void {
		if (this.cleanupScheduled) return;
		this.cleanupScheduled = true;

		if (cleanupOnExit) {
			// Cleanup on process exit
			process.on('exit', () => {
				// Synchronous cleanup for exit handler
				for (const fileInfo of this.activeTempFiles.values()) {
					try {
						if (fileInfo.isValid() && fs.existsSync(fileInfo.path)) {
							fs.unlinkSync(fileInfo.path);
						}
					} catch (error) {
						// Ignore errors during exit cleanup
					}
				}
			});

			// Cleanup on signals
			['SIGTERM', 'SIGINT', 'SIGHUP'].forEach(signal => {
				process.on(signal, async () => {
					await this.cleanupAll();
					process.exit(0);
				});
			});
		}

		// Periodic cleanup of stale files
		resourceManager.createInterval(async () => {
			try {
				await this.cleanupStaleFiles();
			} catch (error) {
				console.warn('Failed to cleanup stale temporary files:', error.message);
			}
		}, 5 * 60 * 1000, 'Secure temp file cleanup');
	}

	/**
	 * Clean up stale temporary files
	 */
	private static async cleanupStaleFiles(): Promise<void> {
		const toDelete = [];
		
		for (const [filepath, fileInfo] of this.activeTempFiles.entries()) {
			if (!fileInfo.isValid()) {
				toDelete.push(filepath);
			} else {
				const stats = fileInfo.getStats();
				if (!stats) {
					// File no longer exists
					toDelete.push(filepath);
				}
			}
		}

		// Remove invalid entries
		for (const filepath of toDelete) {
			this.activeTempFiles.delete(filepath);
		}
	}
}