import path from 'path';
import fs from 'fs/promises';
import { IStorageProvider, UploadResult } from './IStorageProvider';

/**
 * LocalStorageAdapter
 *
 * Stores files on the local filesystem.
 * Files are moved from the multer temp path to the configured storage directory.
 */
export class LocalStorageAdapter implements IStorageProvider {
	private storageRoot: string;
	private publicBaseUrl: string;
	private publicUrlPath: string;
	private readonly MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MB default

	constructor() {
		this.storageRoot = process.env.LOCAL_STORAGE_ROOT ?? path.resolve(process.cwd(), 'storage/uploads');
		this.publicBaseUrl = process.env.PUBLIC_BASE_URL ?? 'http://localhost:3000';
		// Derive public URL path from storage root relative to project root
		const projectRoot = process.cwd();
		this.publicUrlPath = process.env.PUBLIC_STORAGE_PATH ?? path.relative(projectRoot, this.storageRoot);
	}

	async upload(sourcePath: string, destKey: string, _mime: string): Promise<UploadResult> {
		try {
			// SEC: Validate destKey to prevent path traversal attacks
			const normalizedDestKey = destKey.replace(/\\/g, '/');
			const dest = path.join(this.storageRoot, normalizedDestKey);
			const resolvedDest = path.resolve(dest);
			const resolvedRoot = path.resolve(this.storageRoot);

			// Ensure the resolved destination is within the storage root
			if (!resolvedDest.startsWith(resolvedRoot)) {
				throw new Error('Invalid destination path: path traversal detected');
			}

			// Validate file size before upload
			const stats = await fs.stat(sourcePath);
			if (stats.size > this.MAX_FILE_SIZE) {
				throw new Error(`File size exceeds maximum limit of ${this.MAX_FILE_SIZE / (1024 * 1024)} MB`);
			}

			await fs.mkdir(path.dirname(dest), { recursive: true });
			// P2-9: fs.rename fails across devices (EXDEV) — fall back to copy+delete
			try {
				await fs.rename(sourcePath, dest);
			} catch (err: any) {
				if (err.code === 'EXDEV') {
					await fs.copyFile(sourcePath, dest);
					await fs.unlink(sourcePath).catch(() => {});
				} else {
					throw err;
				}
			}

			return {
				storagePath: destKey,
				publicUrl: `${this.publicBaseUrl}/${this.publicUrlPath}/${destKey}`,
				storageProvider: 'local',
			};
		} catch (error) {
			throw error;
		}
	}

	async delete(storagePath: string): Promise<void> {
		// SEC: Validate storagePath to prevent path traversal attacks
		const normalizedStoragePath = storagePath.replace(/\\/g, '/');
		const fullPath = path.join(this.storageRoot, normalizedStoragePath);
		const resolvedPath = path.resolve(fullPath);
		const resolvedRoot = path.resolve(this.storageRoot);

		// Ensure the resolved path is within the storage root
		if (!resolvedPath.startsWith(resolvedRoot)) {
			throw new Error('Invalid storage path: path traversal detected');
		}

		try {
			await fs.unlink(resolvedPath);
		} catch (err: any) {
			// Ignore missing file errors (idempotent delete)
			if (err.code !== 'ENOENT') {
				throw err;
			}
		}
	}

	/**
	 * Clean up old files from storage
	 * @param olderThanDays - Delete files older than this many days (default 30)
	 */
	async cleanupOldFiles(olderThanDays = 30): Promise<number> {
		const cutoffTime = Date.now() - olderThanDays * 24 * 60 * 60 * 1000;
		let deletedCount = 0;

		try {
			// PERF: Use async iteration with setImmediate to avoid blocking event loop
			const walkDir = async (dir: string): Promise<void> => {
				const entries = await fs.readdir(dir, { withFileTypes: true });

				for (const entry of entries) {
					const fullPath = path.join(dir, entry.name);

					if (entry.isDirectory()) {
						await walkDir(fullPath);
						// Yield to event loop periodically
						await new Promise((resolve) => setImmediate(resolve));
					} else if (entry.isFile()) {
						const stats = await fs.stat(fullPath);
						if (stats.mtimeMs < cutoffTime) {
							await fs.unlink(fullPath);
							deletedCount++;
							// Yield to event loop periodically
							if (deletedCount % 100 === 0) {
								await new Promise((resolve) => setImmediate(resolve));
							}
						}
					}
				}
			};

			await walkDir(this.storageRoot);

			// Clean up empty directories
			await this.cleanupEmptyDirectories(this.storageRoot);

			return deletedCount;
		} catch (error) {
			console.error('Error during cleanup:', error);
			throw error;
		}
	}

	/**
	 * Remove empty directories recursively
	 */
	private async cleanupEmptyDirectories(dir: string): Promise<void> {
		try {
			const entries = await fs.readdir(dir, { withFileTypes: true });

			for (const entry of entries) {
				if (entry.isDirectory()) {
					const fullPath = path.join(dir, entry.name);
					await this.cleanupEmptyDirectories(fullPath);
				}
			}

			// Check if directory is now empty
			const remainingEntries = await fs.readdir(dir);
			if (remainingEntries.length === 0 && dir !== this.storageRoot) {
				await fs.rmdir(dir);
			}
		} catch (error) {
			// Ignore errors for directories that no longer exist
			if ((error as any).code !== 'ENOENT') {
				console.error('Error cleaning up directories:', error);
			}
		}
	}

	async getSignedUrl(
		_storagePath: string,
		_expiresInSeconds?: number,
		_contentDisposition?: 'inline' | 'attachment',
	): Promise<string | null> {
		// Local storage does not support signed URLs
		return null;
	}

	/**
	 * Check if a file exists in storage
	 */
	async fileExists(storagePath: string): Promise<boolean> {
		const normalizedStoragePath = storagePath.replace(/\\/g, '/');
		const fullPath = path.join(this.storageRoot, normalizedStoragePath);
		const resolvedPath = path.resolve(fullPath);
		const resolvedRoot = path.resolve(this.storageRoot);

		// Ensure the resolved path is within the storage root
		if (!resolvedPath.startsWith(resolvedRoot)) {
			throw new Error('Invalid storage path: path traversal detected');
		}

		try {
			await fs.access(resolvedPath);
			return true;
		} catch (error) {
			return false;
		}
	}

	/**
	 * Get the size of a file in storage
	 * Returns null if file doesn't exist
	 */
	async getFileSize(storagePath: string): Promise<number | null> {
		const normalizedStoragePath = storagePath.replace(/\\/g, '/');
		const fullPath = path.join(this.storageRoot, normalizedStoragePath);
		const resolvedPath = path.resolve(fullPath);
		const resolvedRoot = path.resolve(this.storageRoot);

		// Ensure the resolved path is within the storage root
		if (!resolvedPath.startsWith(resolvedRoot)) {
			throw new Error('Invalid storage path: path traversal detected');
		}

		try {
			const stats = await fs.stat(resolvedPath);
			return stats.size;
		} catch (error) {
			return null;
		}
	}
}
