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

/**
 * MinioStorageAdapter
 *
 * Stores files in a MinIO (or any S3-compatible) bucket.
 *
 * Required env vars:
 *   MINIO_ENDPOINT   — hostname (e.g., minio or play.min.io)
 *   MINIO_PORT       — port (default 9000)
 *   MINIO_USE_SSL    — "true" | "false" (default "false")
 *   MINIO_ACCESS_KEY — access key / username
 *   MINIO_SECRET_KEY — secret key / password
 *   MINIO_BUCKET     — target bucket name
 *
 * Install before using:
 *   pnpm add minio
 */

// Minimal interface for Minio client to improve type safety
interface MinioClient {
	bucketExists(bucketName: string): Promise<boolean>;
	makeBucket(bucketName: string, region?: string): Promise<void>;
	fPutObject(
		bucketName: string,
		objectName: string,
		filePath: string,
		metaData: Record<string, string>,
	): Promise<string>;
	removeObject(bucketName: string, objectName: string): Promise<void>;
	presignedGetObject(
		bucketName: string,
		objectName: string,
		expires?: number,
		reqParams?: Record<string, string>,
	): Promise<string>;
	presignedPutObject(bucketName: string, objectName: string, expires?: number): Promise<string>;
	statObject(bucketName: string, objectName: string): Promise<{ size: number; metaData: Record<string, string> }>;
}

export class MinioStorageAdapter implements IStorageProvider {
	private client: MinioClient;
	private bucket: string;
	private publicBaseUrl: string;
	/** P2-2: cached once to avoid a network round-trip on every upload */
	private bucketEnsured = false;
	private readonly MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MB default
	private readonly MAX_RETRIES = 3;
	private readonly RETRY_DELAY_MS = 1000;

	constructor() {
		// Lazily imported to avoid hard dependency when not in use.
		// Check if minio package is installed before requiring it
		let Client;
		try {
			// eslint-disable-next-line @typescript-eslint/no-var-requires
			const minioModule = require('minio');
			Client = minioModule.Client;
		} catch (error) {
			throw new Error(
				'MinIO package not installed. Install it with: pnpm add minio\n' +
					'Alternatively, set STORAGE_PROVIDER=local to use local file storage instead.',
			);
		}

		this.bucket = process.env.MINIO_BUCKET || 'uploads';
		const useSSL = process.env.MINIO_USE_SSL === 'true';
		const port = parseInt(process.env.MINIO_PORT || '9000', 10);
		const endpoint = process.env.MINIO_ENDPOINT || 'localhost';

		const accessKey = process.env.MINIO_ACCESS_KEY;
		const secretKey = process.env.MINIO_SECRET_KEY;

		// SEC: Reject empty credentials to prevent unauthorized access
		if (!accessKey || !secretKey) {
			throw new Error('MINIO_ACCESS_KEY and MINIO_SECRET_KEY must be set when using MinIO storage');
		}

		this.client = new Client({
			endPoint: endpoint,
			port,
			useSSL,
			accessKey,
			secretKey,
		});

		const protocol = useSSL ? 'https' : 'http';
		this.publicBaseUrl = `${protocol}://${endpoint}:${port}/${this.bucket}`;
	}

	/**
	 * Retry helper with exponential backoff
	 */
	private async retryWithBackoff<T>(operation: () => Promise<T>, operationName: string): Promise<T> {
		let lastError: Error | undefined;
		for (let attempt = 0; attempt <= this.MAX_RETRIES; attempt++) {
			try {
				return await operation();
			} catch (error) {
				lastError = error as Error;
				const isRetryable = this.isRetryableError(error as Error);

				if (!isRetryable || attempt === this.MAX_RETRIES) {
					throw error;
				}

				const delay = this.RETRY_DELAY_MS * Math.pow(2, attempt);
				console.warn(`Retry attempt ${attempt + 1}/${this.MAX_RETRIES} for ${operationName} after ${delay}ms`);
				await new Promise((resolve) => setTimeout(resolve, delay));
			}
		}
		throw lastError;
	}

	/**
	 * Check if error is retryable
	 */
	private isRetryableError(error: Error): boolean {
		const retryableCodes = ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND'];
		const retryableStatusCodes = [503, 504, 429];
		const errorMessage = error.message.toLowerCase();

		// Network errors
		if (retryableCodes.some((code) => errorMessage.includes(code.toLowerCase()))) {
			return true;
		}

		// HTTP status codes
		if ((error as any).statusCode && retryableStatusCodes.includes((error as any).statusCode)) {
			return true;
		}

		// MinIO specific errors
		if (errorMessage.includes('timeout') || errorMessage.includes('connection')) {
			return true;
		}

		return false;
	}

	async upload(sourcePath: string, destKey: string, mime: string): Promise<UploadResult> {
		try {
			// 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`);
			}

			// P2-2: ensure bucket exists only once per adapter instance
			if (!this.bucketEnsured) {
				await this.retryWithBackoff(async () => {
					const exists = await this.client.bucketExists(this.bucket);
					if (!exists) {
						await this.client.makeBucket(this.bucket, process.env.MINIO_REGION || 'us-east-1');
					}
				}, 'bucketExists/makeBucket');
				this.bucketEnsured = true;
			}

			// Normalise key: strip leading slashes, use forward slashes
			const key = destKey.replace(/\\/g, '/').replace(/^\/+/, '');

			await this.retryWithBackoff(
				() => this.client.fPutObject(this.bucket, key, sourcePath, { 'Content-Type': mime }),
				'fPutObject',
			);
			// P1-1: remove temp file from disk after successful upload to object storage
			await fs.unlink(sourcePath).catch(() => {});

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

	async delete(storagePath: string): Promise<void> {
		try {
			const key = storagePath.replace(/\\/g, '/').replace(/^\/+/, '');
			await this.retryWithBackoff(() => this.client.removeObject(this.bucket, key), 'removeObject');
		} catch (error) {
			throw error;
		}
	}

	/** SEC-05: pass response-content-disposition so MinIO sets the header on the served object */
	async getSignedUrl(
		storagePath: string,
		expiresInSeconds = 3600,
		contentDisposition?: 'inline' | 'attachment',
		filename?: string,
	): Promise<string | null> {
		try {
			const key = storagePath.replace(/\\/g, '/').replace(/^\/+/, '');
			const reqParams: Record<string, string> = {};
			if (contentDisposition) {
				// Include filename parameter for proper download behavior
				// SEC: Properly encode filename to prevent header injection and handle non-ASCII characters
				let dispositionValue: string = contentDisposition;
				if (filename) {
					// Escape special characters in filename to prevent header injection
					const escapedFilename = filename.replace(/["\\]/g, '\\$&');
					// Use ASCII-only encoding for filename parameter
					const asciiFilename = escapedFilename.replace(/[^\x00-\x7F]/g, (char) => {
						return encodeURIComponent(char).replace(/%/g, '_');
					});
					dispositionValue = `${contentDisposition}; filename="${asciiFilename}"`;
				}
				reqParams['response-content-disposition'] = dispositionValue;
			}
			const url: string = await this.client.presignedGetObject(this.bucket, key, expiresInSeconds, reqParams);
			return url;
		} catch (error) {
			throw error;
		}
	}

	/**
	 * Generate a pre-signed PUT URL for direct browser uploads (optional helper).
	 */
	async getPresignedPutUrl(destKey: string, expiresInSeconds = 3600): Promise<string> {
		const key = destKey.replace(/\\/g, '/').replace(/^\/+/, '');
		return this.client.presignedPutObject(this.bucket, key, expiresInSeconds);
	}

	/**
	 * Check whether an object key exists in the bucket.
	 * Used by /files/confirm to verify the client actually uploaded the file.
	 */
	async objectExists(storagePath: string): Promise<boolean> {
		const key = storagePath.replace(/\\/g, '/').replace(/^\/+/, '');
		try {
			await this.client.statObject(this.bucket, key);
			return true;
		} catch (err: any) {
			// MinIO returns specific error codes for not-found (e.g., NoSuchKey)
			// Only return false for not-found, rethrow other errors
			if (err.code === 'NoSuchKey' || err.code === 'NotFound' || err.statusCode === 404) {
				return false;
			}
			throw err;
		}
	}

	/**
	 * Get the actual size of an object in the bucket.
	 * Used by /files/confirm to verify the uploaded file size matches the declared size.
	 * Returns null if object doesn't exist or error occurs.
	 */
	async getObjectSize(storagePath: string): Promise<number | null> {
		const key = storagePath.replace(/\\/g, '/').replace(/^\/+/, '');
		try {
			const stat = await this.client.statObject(this.bucket, key);
			return stat.size;
		} catch (err: any) {
			// Return null for not-found, rethrow other errors
			if (err.code === 'NoSuchKey' || err.code === 'NotFound' || err.statusCode === 404) {
				return null;
			}
			throw err;
		}
	}
}
